def __init__(self, id, title, languages=[('en', 'English')]):
     self.id = id
     self.title = title
     n_languages = []
     for (code, name) in languages:
         n_languages.append((normalize_code(code), name))
     self._portal_langs = NyPortalLanguageManager(n_languages)
     lang_codes = tuple([x[0] for x in n_languages])
     catalog = NyMessageCatalog('translation_catalog',
                                'Translation Catalog', lang_codes)
     self._catalog = catalog
 def __init__(self, id, title, languages=[('en', 'English')]):
     self.id = id
     self.title = title
     n_languages = []
     for (code, name) in languages:
         n_languages.append((normalize_code(code), name))
     self._portal_langs = NyPortalLanguageManager(n_languages)
     lang_codes = tuple([x[0] for x in n_languages])
     catalog = NyMessageCatalog('translation_catalog', 'Translation Catalog',
                                 lang_codes)
     self._catalog = catalog
class NaayaI18n(Folder):
    """
    Naaya instantiates a **NaayaI18n** object insides its root. This object
    holds the whole internationalization data and operations:
    - management of languages
    - negotiation
    - translating and translation data
    - management of object property localization

    """

    meta_type = METATYPE_NAAYAI18N
    icon = 'misc_/portal_i18n/icon.gif'

    security = ClassSecurityInfo()

    def __init__(self, id, title, languages=[('en', 'English')]):
        self.id = id
        self.title = title
        n_languages = []
        for (code, name) in languages:
            n_languages.append((normalize_code(code), name))
        self._portal_langs = NyPortalLanguageManager(n_languages)
        lang_codes = tuple([x[0] for x in n_languages])
        catalog = NyMessageCatalog('translation_catalog', 'Translation Catalog',
                                    lang_codes)
        self._catalog = catalog

    def get_negotiator(self):
        """ Return NyNegotiator instance, based on current request """
        try:
            return NyNegotiator(request=self.getSite().REQUEST)
        except AttributeError:
            # maybe debug/testing environment?
            return NyNegotiator()

    def get_message_catalog(self):
        """
        Returns Message Catalog (NyMessageCatalog instance).
        The Message Catalog stores translations for all messages.

        """
        return self._catalog

    def get_lang_manager(self):
        """
        Returns a NyLanguages instance, capable of managing
        a list of ISO 639 language code and names, based on languages.txt
        in naaya.i18n

        """
        if not hasattr(self, 'lang_manager'):
            self.lang_manager = NyLanguages()
        return self.lang_manager

    def get_portal_lang_manager(self):
        """
        Returns NyPortalLanguageManager instance in portal, capable
        of managing available languages

        """
        return self._portal_langs

    def get_importexport_tool(self):
        """ Returns the import-export wrapper for Message Catalog """
        return TranslationsImportExport(self.get_message_catalog())

    ### More specific methods:
    def get_language_name(self, code):
        """
        Get the language name for 'code'. It first looks up
        languages manually added in portal, then asks for languages
        in NyLanguages (naaya.i18n/languages.txt) which finally falls back
        to '???' string

        """
        if code in self.get_portal_lang_manager().getAvailableLanguages():
            # try to get name from added langs to site
            return self.get_portal_lang_manager().get_language_name(code)
        else:
            # not there, default to NyLanguages: loads the large languages.txt
            return self.get_lang_manager().get_language_name(code)

    security.declarePublic('get_languages_mapping')
    def get_languages_mapping(self):
        """
        Returns
        [{'code': 'xx', 'name': 'Xxx xx', default: True/False}, .. ]
        for languages currently available in portal

        """
        langs = list(self._portal_langs.getAvailableLanguages())
        langs.sort()
        result = []
        default = self._portal_langs.get_default_language()
        for l in langs:
            result.append({'code': l,
                           'name': self.get_language_name(l),
                           'default': l == default})
        return result

    def add_language(self, lang_code, lang_name=None):
        """
        Adds a new supported language in portal_i18n.
        Language code, language name can be any combination.
        If language name is not provided,
        a lookup is being performed in naaya.i18n/languages.txt

        """
        if not lang_code:
            raise ValueError('No language code provided')
        lang_code = normalize_code(lang_code)
        if lang_name is None:
            # search for name directly in languages.txt, obviously not in site
            lang_name = self.get_lang_manager().get_language_name(lang_code)
        # add language to portal:
        self._portal_langs.addAvailableLanguage(lang_code, lang_name)
        # and to catalog:
        self._catalog.add_language(lang_code)

    def del_language(self, lang):
        """
        Deletes a language from portal_i18n:
         * removes it from available languages
         * removes it from supported languages in message catalog

        """
        self._portal_langs.delAvailableLanguage(lang)
        self._catalog.del_language(lang)

    def get_selected_language(self, context=None):
        """ Performs negotiation and returns selected languag based on context
        or finds context using threading.local patch """
        return self.get_negotiator().getLanguage(
                            self._portal_langs.getAvailableLanguages(), context)

    def change_selected_language(self, lang, goto=None, expires=None):
        """ Sets a cookie with a new preferred selected language """
        request = self.REQUEST
        response = request.RESPONSE
        negotiator = self.get_negotiator()

        # Changes the cookie (it could be something different)
        parent = self.aq_parent
        path = parent.absolute_url()[len(request['SERVER_URL']):] or '/'
        if expires is None:
            response.setCookie(negotiator.cookie_id, lang, path=path)
        else:
            response.setCookie(negotiator.cookie_id, lang, path=path,
                               expires=unquote(expires))
        # Comes back
        if goto is None:
            goto = request['HTTP_REFERER']

        response.redirect(goto)

    ### Public i18n related methods
    security.declarePublic('get_message_translation')
    def get_message_translation(self, message='', lang=None, default=None):
        """
        Returns the translation of the given message in the given language,
        as it is stored in Message Catalog (no interpolation).

        """
        if message == '':
            return None
        if not lang:
            lang = self.get_selected_language()
        return self.get_message_catalog().gettext(message, lang, default)

    security.declarePublic('get_translation')
    def get_translation(self, msg, **kwargs):
        """
        Translate message using Message Catalog
        and substitute named identifiers with values supplied by kwargs mapping

        """
        lang = self.get_selected_language()
        msg = self.get_message_catalog().gettext(msg, lang)
        return interpolate(msg, kwargs)

    ### Public general purpose methods
    security.declarePublic('message_decode')
    def message_decode(self, message):
        """
        Decodes a message from an ASCII string.

        To be used in the user interface, to avoid problems with the
        encodings, HTML entities, etc..

        """
        message = decodestring(message)
        encoding = HTTPRequest.default_encoding
        return unicode(message, encoding)

    security.declarePublic('message_decode')
    def message_encode(self, message):
        """
        Encodes a message to an ASCII string.

        To be used in the user interface, to avoid problems with the
        encodings, HTML entities, etc..

        """
        if isinstance(message, unicode):
            encoding = HTTPRequest.default_encoding
            message = message.encode(encoding)
    
        return encodestring(message)

    ### Private methods for private views

    security.declareProtected('Manage messages', 'manage_messages')
    def external_translate(self, message, target_lang):
        """
        Private method that returns a translation based on an external
        service, e.g. Google Translate. Not public because the number of
        requests must not be abusive

        """
        return external_translate(message, target_lang)

    #######################################################################
    # Management screens
    #######################################################################
    def manage_options(self):
        """ """
        options = (
            {'label': u'Messages', 'action': 'manage_messages'},
            {'label': u'Languages', 'action': 'manage_languages'},
            {'label': u'Import', 'action': 'manage_import'
            },
            {'label': u'Export', 'action': 'manage_export'
            }) \
            + SimpleItem.manage_options
        r = []
        for option in options:
            option = option.copy()
            r.append(option)
        return r

    #### Messages Tab ####

    security.declarePublic('get_namespace')
    def get_namespace(self, REQUEST):
        """For the management interface, allows to filter the messages to
        show.
        """
        # Check whether there are languages or not
        languages = self.get_languages_mapping()
        if not languages:
            return {}

        # Input
        batch_start = REQUEST.get('batch_start', 0)
        batch_size = REQUEST.get('batch_size', 15)
        empty = REQUEST.get('empty', 0)
        regex = REQUEST.get('regex', '')
        message = REQUEST.get('msg', None)

        # Build the namespace
        namespace = {}
        namespace['batch_size'] = batch_size
        namespace['empty'] = empty
        namespace['regex'] = regex

        # The language
        lang = REQUEST.get('lang', None) or languages[0]['code']
        namespace['language'] = lang

        # Filter the messages
        query = regex.strip()
        try:
            query = re.compile(query)
        except:
            query = re.compile('')

        messages = []
        for m, t in self.get_message_catalog().messages():
            if query.search(m) and (not empty or not t.get(lang, '').strip()):
                messages.append(m)
        messages.sort(filter_sort)
        # How many messages
        n = len(messages)
        namespace['n_messages'] = n

        # Calculate the start
        while batch_start >= n:
            batch_start = batch_start - batch_size
        if batch_start < 0:
            batch_start = 0
        namespace['batch_start'] = batch_start
        # Select the batch to show
        batch_end = batch_start + batch_size
        messages = messages[batch_start:batch_end]
        # Batch links
        namespace['previous'] = get_url(REQUEST.URL, batch_start - batch_size,
            batch_size, regex, lang, empty)
        namespace['next'] = get_url(REQUEST.URL, batch_start + batch_size,
            batch_size, regex, lang, empty)

        # Get the message
        message_encoded = None
        #translations = {}
        translation = None
        if message is None:
            if messages:
                message = messages[0]
                translation = self.get_message_catalog()\
                                  .gettext(message, lang, '')
                message = to_unicode(message)
                message_encoded = self.message_encode(message)
        else:
            message_encoded = message
            message = self.message_decode(message_encoded)
            #translations = self.get_translations(message)
            translation = self.get_message_catalog().gettext(message, lang, '')
            message = to_unicode(message)
        namespace['message'] = message
        namespace['message_encoded'] = message_encoded
        #namespace['translations'] = translations
        namespace['translation'] = translation

        # Calculate the current message
        namespace['messages'] = []
        for x in messages:
            x = to_unicode(x)
            x_encoded = self.message_encode(x)
            url = get_url(
                REQUEST.URL, batch_start, batch_size, regex, lang, empty,
                msg=x_encoded)
            namespace['messages'].append({
                'message': x,
                'message_encoded': x_encoded,
                'current': x == message,
                'url': url})

        # The languages
        for language in languages:
            code = language['code']
            language['name'] = _(language['name'])
            language['url'] = get_url(REQUEST.URL, batch_start, batch_size,
                regex, code, empty, msg=message_encoded)
        namespace['languages'] = languages

        return namespace

    security.declareProtected('Manage messages', 'manage_messages')
    manage_messages = DTMLFile('zpt/messages', globals())

    security.declareProtected('Manage messages', 'manage_editMessage')
    def manage_editMessage(self, message, language, translation,
                           REQUEST, RESPONSE):
        """Modifies a message.
        """
        message_encoded = message
        message_key = self.message_decode(message_encoded)
        self.get_message_catalog()\
            .edit_message(message_key, language, translation)

        url = get_url(REQUEST.URL1 + '/manage_messages',
                      REQUEST['batch_start'], REQUEST['batch_size'],
                      REQUEST['regex'], REQUEST.get('lang', ''),
                      REQUEST.get('empty', 0),
                      msg=message_encoded,
                      manage_tabs_message=_(u'Saved changes.'))
        RESPONSE.redirect(url)

    security.declareProtected('Manage messages', 'manage_delMessage')
    def manage_delMessage(self, message, REQUEST, RESPONSE):
        """ """
        message_key = self.message_decode(message)
        self.get_message_catalog().del_message(message_key)

        url = get_url(REQUEST.URL1 + '/manage_messages',
                      REQUEST['batch_start'], REQUEST['batch_size'],
                      REQUEST['regex'], REQUEST.get('lang', ''),
                      REQUEST.get('empty', 0),
                      manage_tabs_message=_(u'Saved changes.'))
        RESPONSE.redirect(url)

    #### Languages Tab ####

    security.declareProtected(view_management_screens, 'manage_languages')
    manage_languages = PageTemplateFile('zpt/languages', globals())

    security.declareProtected(view_management_screens, 'manage_addLanguage')
    def manage_addLanguage(self, language_code, language_name, REQUEST=None):
        """
        Add a new language for this portal.
        """
        self.getSite().gl_add_site_language(language_code, language_name)
        if REQUEST: REQUEST.RESPONSE.redirect('manage_languages?save=ok')

    security.declareProtected(view_management_screens, 'manage_delLanguages')
    def manage_delLanguages(self, languages, REQUEST=None):
        """
        Delete one or more languages.
        """
        self.getSite().gl_del_site_languages(languages)
        if REQUEST: REQUEST.RESPONSE.redirect('manage_languages?save=ok')

    security.declareProtected(view_management_screens,
                              'manage_changeDefaultLang')
    def manage_changeDefaultLang(self, language, REQUEST=None):
        """
        Change the default portal language.
        """
        self.getSite().gl_change_site_defaultlang(language)
        if REQUEST: REQUEST.RESPONSE.redirect('manage_languages?save=ok')

    #### Export Tab ####

    security.declareProtected(view_management_screens, 'manage_export')
    manage_export = PageTemplateFile('zpt/export_form', globals())

    security.declareProtected(view_management_screens, 'manage_export_po')
    def manage_export_po(self, language, REQUEST, RESPONSE):
        """ Provides pot/po export file for download """
        export_tool = self.get_importexport_tool()
        if language == 'locale.pot':
            filename = language
        else:
            filename = '%s.po' % language
        RESPONSE.setHeader('Content-type','application/data')
        RESPONSE.setHeader('Content-Disposition',
                           'inline;filename=%s' % filename)
        return export_tool.export_po(language)

    security.declareProtected(view_management_screens, 'manage_export_xliff')
    def manage_export_xliff(self, export_all, language, REQUEST, RESPONSE):
        """ Provides xliff file for download """
        fname = ('%s_%s.xlf'
                 % (self.get_message_catalog()._default_language, language))
        # Generate the XLIFF file header
        RESPONSE.setHeader('Content-Type', 'text/xml; charset=UTF-8')
        RESPONSE.setHeader('Content-Disposition',
                           'attachment; filename="%s"' % fname)
        export_tool = self.get_importexport_tool()
        return export_tool.export_xliff(language,
                                        export_all=bool(int(export_all)))

    security.declareProtected(view_management_screens, 'manage_export_tmx')
    def manage_export_tmx(self, REQUEST, RESPONSE):
        """ Provides tmx file for download """
        cat = self.get_message_catalog()
        fname = '%s.tmx' % cat.title
        export_tool = self.get_importexport_tool()
        header = export_tool.get_po_header(cat._default_language)
        charset = header['charset']
        # Generate the XLIFF file header
        RESPONSE.setHeader('Content-Type', 'text/xml; charset=%s' % charset)
        RESPONSE.setHeader('Content-Disposition',
                           'attachment; filename="%s"' % fname)
        return export_tool.export_tmx()

    #### Import Tab ####

    security.declareProtected(view_management_screens, 'manage_import')
    manage_import = PageTemplateFile('zpt/import_form', globals())

    security.declareProtected(view_management_screens, 'manage_import_po')
    def manage_import_po(self, file, language, REQUEST, RESPONSE):
        """ Import PO file into catalog, for an existing language """
        if language not in self.get_portal_lang_manager().getAvailableLanguages():
            raise ValueError('%s language is not available in portal' % language)
        else:
            import_tool = self.get_importexport_tool()
            import_tool.import_po(language, file)
        RESPONSE.redirect('manage_import?save=ok')

    security.declareProtected(view_management_screens, 'manage_import_tmx')
    def manage_import_tmx(self, file, language, REQUEST, RESPONSE):
        raise NotImplementedError("Tmx import is not yet implemented")

    security.declareProtected(view_management_screens, 'manage_import_xliff')
    def manage_import_xliff(self, file, language, REQUEST, RESPONSE):
        raise NotImplementedError("Xliff import is not yet implemented")
Example #4
0
class NaayaI18n(SimpleItem):
    """
    Naaya instantiates a **NaayaI18n** object insides its root. This object
    holds the whole internationalization data and operations:
    - management of languages
    - negotiation
    - translating and translation data
    - management of object property localization

    """

    meta_type = METATYPE_NAAYAI18N
    icon = 'misc_/portal_i18n/icon.gif'

    security = ClassSecurityInfo()

    message_debug_list = ()
    message_debug_exception = False

    def __init__(self, id, title, languages=[('en', 'English')]):
        self.id = id
        self.title = title
        n_languages = []
        for (code, name) in languages:
            n_languages.append((normalize_code(code), name))
        self._portal_langs = NyPortalLanguageManager(n_languages)
        lang_codes = tuple([x[0] for x in n_languages])
        catalog = NyMessageCatalog('translation_catalog', 'Translation Catalog',
                                    lang_codes)
        self._catalog = catalog

    ### Getters ###

    security.declarePrivate('get_negotiator')
    def get_negotiator(self):
        """ Return NyNegotiator instance, based on current request """
        return NyNegotiator()

    security.declarePrivate('get_message_catalog')
    def get_message_catalog(self):
        """
        Returns Message Catalog (NyMessageCatalog instance).
        The Message Catalog stores translations for all messages.

        """
        return self._catalog

    security.declarePrivate('get_lang_manager')
    def get_lang_manager(self):
        """
        Returns NyPortalLanguageManager instance in portal, capable
        of managing available languages

        """
        return self._portal_langs

    security.declarePrivate('get_importexport_tool')
    def get_importexport_tool(self):
        """ Returns the import-export wrapper for Message Catalog """
        return TranslationsImportExport(self.get_message_catalog())

    ### More specific language methods ###

    security.declarePublic('get_language_name')
    def get_language_name(self, code):
        """
        Get the language name for 'code'. First it looks up
        languages manually added in portal, then inquiries languages
        in naaya.i18n/languages.txt which finally falls back
        to '???' string

        """
        lang_manager = self.get_lang_manager()
        if code in lang_manager.getAvailableLanguages():
            # try to get name from added langs to site
            return lang_manager.get_language_name(code)
        else:
            # not there, default to languages.txt
            return get_iso639_name(code)

    security.declarePublic('get_languages_mapping')
    def get_languages_mapping(self):
        """
        Returns a list of mappings
        [ {'code': 'lang-code', 'name': 'Language name'}, .. ]
        containing languages currently available in portal.

        """
        langs = list(self._portal_langs.getAvailableLanguages())
        result = []
        default = self._portal_langs.get_default_language()
        for l in langs:
            result.append({'code': l,
                           'name': self.get_language_name(l),
                           'default': l == default})
        return result

    security.declarePrivate('add_language')
    def add_language(self, lang_code, lang_name=None):
        """
        Adds a new supported language in portal_i18n.
        Language code, language name can be any combination.
        If language name is not provided,
        a lookup is being performed in naaya.i18n/languages.txt

        """
        if not lang_code:
            raise ValueError('No language code provided')
        lang_code = normalize_code(lang_code)
        if lang_name is None:
            # search for name directly in languages.txt, obviously not in site
            lang_name = get_iso639_name(lang_code)
        # add language to portal:
        self._portal_langs.addAvailableLanguage(lang_code, lang_name)
        # and to catalog:
        self._catalog.add_language(lang_code)

    security.declarePrivate('del_language')
    def del_language(self, lang):
        """
        Deletes a language from portal_i18n:
         * removes it from available languages
         * removes it from supported languages in message catalog

        """
        self._portal_langs.delAvailableLanguage(lang)
        self._catalog.del_language(lang)

    security.declarePublic('get_selected_language')
    def get_selected_language(self, context=None):
        """
        Performs negotiation and returns selected language based on context
        or grabs context (request) by acquisition.

        """
        if context is None:
            context = self.getSite().REQUEST
        available_languages = self.get_lang_manager().getAvailableLanguages()
        return self.get_negotiator().getLanguage(available_languages, context)

    security.declarePublic('get_default_language')
    def get_default_language(self):
        """ Returns default language in portal """
        return self.get_lang_manager().get_default_language()

    security.declarePublic('change_selected_language')
    def change_selected_language(self, lang, goto=None):
        """ Sets a cookie with a new preferred selected language """
        request = self.REQUEST
        response = request.RESPONSE
        negotiator = self.get_negotiator()

        negotiator.change_language(lang, self, request)

        # Comes back
        if goto is None:
            goto = request['HTTP_REFERER']

        response.redirect(goto)

    ### Translation API ###

    security.declarePublic('get_translation')
    def get_translation(self, msg, _default=None, **kwargs):
        """
        Translate message in selected language using Message Catalog
        and substitute named identifiers with values supplied by kwargs mapping

        Most commonly used - a straight forward complete translation
        in selected language.

        """
        lang = kwargs.get('lang') or self.get_selected_language()
        msg = self.get_message_catalog().gettext(msg, lang, default=_default)
        return interpolate(msg, kwargs)

    ### Private methods for private views

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'external_translate')
    def external_translate(self, message, target_lang):
        """
        Private method that returns a translation based on an external
        service, e.g. Google Translate. Not public because the number of
        requests must not be abusive

        """
        return external_translate(message, target_lang)

    #######################################################################
    # Management screens
    #######################################################################
    def manage_options(self):
        """ """
        options = (
            {'label': u'Messages', 'action': 'manage_messages'},
            {'label': u'Languages', 'action': 'manage_languages'},
            {'label': u'Import', 'action': 'manage_import'},
            {'label': u'Export', 'action': 'manage_export'},
            {'label': u'Debug', 'action': 'manage_debug'},
            ) + SimpleItem.manage_options
        r = []
        for option in options:
            option = option.copy()
            r.append(option)
        return r

    #### Messages Tab ####

    security.declarePublic('get_namespace')
    def get_namespace(self, REQUEST):
        """For the management interface, allows to filter the messages to
        show.
        """
        # Check whether there are languages or not
        languages = self.get_languages_mapping()
        if not languages:
            return {}

        # Input
        batch_start = REQUEST.get('batch_start', 0)
        batch_size = REQUEST.get('batch_size', 15)
        empty = REQUEST.get('empty', 0)
        regex = REQUEST.get('regex', '')
        message = REQUEST.get('msg', None)

        # Build the namespace
        namespace = {}
        namespace['batch_size'] = batch_size
        namespace['empty'] = empty
        namespace['regex'] = regex

        # The language
        lang = REQUEST.get('lang', None) or languages[0]['code']
        namespace['language'] = lang

        # Filter the messages
        query = regex.strip()
        try:
            query = re.compile(query)
        except:
            query = re.compile('')

        messages = []
        for m, t in self.get_message_catalog().messages():
            if query.search(m) and (not empty or not t.get(lang, '').strip()):
                messages.append(m)
        messages.sort()
        # How many messages
        n = len(messages)
        namespace['n_messages'] = n

        # Calculate the start
        while batch_start >= n:
            batch_start = batch_start - batch_size
        if batch_start < 0:
            batch_start = 0
        namespace['batch_start'] = batch_start
        # Select the batch to show
        batch_end = batch_start + batch_size
        messages = messages[batch_start:batch_end]
        # Batch links
        namespace['previous'] = get_url(REQUEST.URL, batch_start - batch_size,
            batch_size, regex, lang, empty)
        namespace['next'] = get_url(REQUEST.URL, batch_start + batch_size,
            batch_size, regex, lang, empty)

        # Get the message
        message_encoded = None
        #translations = {}
        translation = None
        if message is None:
            if messages:
                message = messages[0]
                translation = self.get_message_catalog()\
                                  .gettext(message, lang, '')
                message_encoded = message_encode(message)
        else:
            message_encoded = message
            message = message_decode(message_encoded)
            #translations = self.get_translations(message)
            translation = self.get_message_catalog().gettext(message, lang, '')
        namespace['message'] = message
        namespace['message_encoded'] = message_encoded
        #namespace['translations'] = translations
        namespace['translation'] = translation

        # Calculate the current message
        namespace['messages'] = []
        for x in messages:
            x_encoded = message_encode(x)
            url = get_url(
                REQUEST.URL, batch_start, batch_size, regex, lang, empty,
                msg=x_encoded)
            namespace['messages'].append({
                'message': x,
                'message_encoded': x_encoded,
                'current': x == message,
                'url': url})

        # The languages
        for language in languages:
            code = language['code']
            language['name'] = self.get_translation(unicode(language['name']))
            language['url'] = get_url(REQUEST.URL, batch_start, batch_size,
                regex, code, empty, msg=message_encoded)
        namespace['languages'] = languages

        return namespace

    security.declareProtected('Manage messages', 'manage_messages')
    manage_messages = DTMLFile('zpt/messages', globals())

    security.declareProtected('Manage messages', 'manage_editMessage')
    def manage_editMessage(self, message, language, translation,
                           REQUEST, RESPONSE):
        """Modifies a message.
        """
        message_encoded = message
        message_key = message_decode(message_encoded)
        self.get_message_catalog()\
            .edit_message(message_key, language, translation)

        url = get_url(REQUEST.URL1 + '/manage_messages',
                      REQUEST['batch_start'], REQUEST['batch_size'],
                      REQUEST['regex'], REQUEST.get('lang', ''),
                      REQUEST.get('empty', 0),
                      msg=message_encoded,
                      manage_tabs_message=self.get_translation(u'Saved changes.'))
        RESPONSE.redirect(url)

    security.declareProtected('Manage messages', 'manage_delMessage')
    def manage_delMessage(self, message, REQUEST, RESPONSE):
        """Deletes a message in catalog"""
        message_key = message_decode(message)
        self.get_message_catalog().del_message(message_key)

        url = get_url(REQUEST.URL1 + '/manage_messages',
                      REQUEST['batch_start'], REQUEST['batch_size'],
                      REQUEST['regex'], REQUEST.get('lang', ''),
                      REQUEST.get('empty', 0),
                      manage_tabs_message=self.get_translation(u'Saved changes.'))
        RESPONSE.redirect(url)

    #### Languages Tab ####

    security.declareProtected(view_management_screens, 'manage_languages')
    manage_languages = PageTemplateFile('zpt/languages', globals())

    security.declareProtected(view_management_screens, 'manage_addLanguage')
    def manage_addLanguage(self, language_code, language_name, REQUEST=None):
        """
        Add a new language for this portal.
        """
        self.getSite().gl_add_site_language(language_code, language_name)
        if REQUEST is not None:
            REQUEST.RESPONSE.redirect('manage_languages?save=ok')

    security.declareProtected(view_management_screens, 'manage_delLanguages')
    def manage_delLanguages(self, languages=[], display_order='', REQUEST=None):
        """
        Delete one or more languages. Also handles move up/down language
        in display order.

        """
        if languages:
            self.getSite().gl_del_site_languages(languages)
        if display_order:
            lang_manager = self.get_lang_manager()
            lang_manager.set_display_order(display_order)
        if REQUEST is not None:
            REQUEST.RESPONSE.redirect('manage_languages?save=ok')

    security.declareProtected(view_management_screens,
                              'manage_changeDefaultLang')
    def manage_changeDefaultLang(self, language, REQUEST=None):
        """
        Change the default portal language.
        """
        self.getSite().gl_change_site_defaultlang(language)
        if REQUEST is not None:
            REQUEST.RESPONSE.redirect('manage_languages?save=ok')

    #### Export Tab ####

    security.declareProtected(view_management_screens, 'manage_export')
    manage_export = PageTemplateFile('zpt/export_form', globals())

    security.declareProtected(view_management_screens, 'manage_export_po')
    def manage_export_po(self, language, REQUEST, RESPONSE):
        """ Provides pot/po export file for download """
        export_tool = self.get_importexport_tool()
        if language == 'locale.pot':
            filename = language
        else:
            filename = '%s.po' % language
        RESPONSE.setHeader('Content-type','application/data')
        RESPONSE.setHeader('Content-Disposition',
                           'inline;filename=%s' % filename)
        return export_tool.export_po(language)

    security.declareProtected(view_management_screens, 'manage_export_xliff')
    def manage_export_xliff(self, export_all, language, REQUEST, RESPONSE):
        """ Provides xliff file for download """
        fname = ('%s_%s.xlf'
                 % (self.get_message_catalog()._default_language, language))
        # Generate the XLIFF file header
        RESPONSE.setHeader('Content-Type', 'text/xml; charset=UTF-8')
        RESPONSE.setHeader('Content-Disposition',
                           'attachment; filename="%s"' % fname)
        export_tool = self.get_importexport_tool()
        return export_tool.export_xliff(language,
                                        export_all=bool(int(export_all)))

    security.declareProtected(view_management_screens, 'manage_export_tmx')
    def manage_export_tmx(self, REQUEST, RESPONSE):
        """ Provides tmx file for download """
        cat = self.get_message_catalog()
        fname = '%s.tmx' % cat.title
        export_tool = self.get_importexport_tool()
        header = export_tool.get_po_header(cat._default_language)
        charset = header['charset']
        # Generate the XLIFF file header
        RESPONSE.setHeader('Content-Type', 'text/xml; charset=%s' % charset)
        RESPONSE.setHeader('Content-Disposition',
                           'attachment; filename="%s"' % fname)
        return export_tool.export_tmx()

    #### Import Tab ####

    security.declareProtected(view_management_screens, 'manage_import')
    manage_import = PageTemplateFile('zpt/import_form', globals())

    security.declareProtected(view_management_screens, 'manage_import_po')
    def manage_import_po(self, file, language, REQUEST, RESPONSE):
        """ Import PO file into catalog, for an existing language """
        if language not in self.get_lang_manager().getAvailableLanguages():
            raise ValueError('%s language is not available in portal' % language)
        else:
            import_tool = self.get_importexport_tool()
            import_tool.import_po(language, file)
        RESPONSE.redirect('manage_import?save=ok')

    security.declareProtected(view_management_screens, 'manage_import_tmx')
    def manage_import_tmx(self, file, language, REQUEST, RESPONSE):
        raise NotImplementedError("Tmx import is not yet implemented")

    security.declareProtected(view_management_screens, 'manage_import_xliff')
    def manage_import_xliff(self, file, language, REQUEST, RESPONSE):
        raise NotImplementedError("Xliff import is not yet implemented")

    _manage_debug = PageTemplateFile('zpt/debug', globals())
    security.declareProtected(view_management_screens, 'manage_debug')
    def manage_debug(self, REQUEST):
        """ Watch for problematic translation strings """

        if REQUEST.REQUEST_METHOD == 'POST':
            form = REQUEST.form
            self.message_debug_list = form['debug_strings'].splitlines()
            log.info('%s message_debug_list = %r',
                     physical_path(self), self.message_debug_list)
            self.message_debug_exception = bool(form.get('debug_exception', False))

            location = self.absolute_url() + '/manage_debug'
            return REQUEST.RESPONSE.redirect(location)

        return self._manage_debug(REQUEST)

    #######################################################################
    # Naaya Administration screens
    #######################################################################

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'get_admin_i18n')
    def get_admin_i18n(self):
        """
        Return a wrapper for portal_i18n, providing an interface used
        in rendering admin views and performing admin actions.

        """
        return AdminI18n(self).__of__(self)

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_translations_html')
    admin_translations_html = PageTemplateFile('zpt/site_admin_translations', globals())

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_messages_html')
    admin_messages_html = PageTemplateFile('zpt/site_admin_messages', globals())

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_importexport_html')
    admin_importexport_html = PageTemplateFile('zpt/site_admin_importexport', globals())

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_editmessage')
    def admin_editmessage(self, message, language, translation, start, skey, rkey, query, REQUEST=None):
        """ """
        ob = self.get_message_catalog()
        ob.edit_message(message_decode(message), language, translation)
        if REQUEST:
            self.setSessionInfoTrans(MESSAGE_SAVEDCHANGES, date=self.utGetTodayDate())
            REQUEST.RESPONSE.redirect('%s/admin_messages_html?msg=%s&start=%s&skey=%s&rkey=%s&query=%s&trans_lang=%s&saved=True' % \
                (self.absolute_url(), quote(message), start, skey, rkey, query, language))

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_delmesg_html')
    admin_delmesg_html = PageTemplateFile('zpt/site_admin_delmessages', globals())

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_delmsg')
    def admin_delmsg(self, messages=[], REQUEST=None):
        """ """
        message_catalog = self.get_message_catalog()
        messages = self.utConvertToList(messages)
        for message in messages:
            message_catalog.del_message(message)
        if REQUEST:
            self.getSite().setSessionInfoTrans(MESSAGE_SAVEDCHANGES, date=self.utGetTodayDate())
            REQUEST.RESPONSE.redirect('%s/admin_delmesg_html' % self.absolute_url())

    security.declareProtected(PERMISSION_PUBLISH_OBJECTS, 'admin_basket_translations_html')
    admin_basket_translations_html = PageTemplateFile('zpt/site_admin_basket_translations', globals())

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_exportmessages')
    def admin_exportmessages(self, x, REQUEST=None, RESPONSE=None):
        """ """
        return self.manage_export_po(x, REQUEST, RESPONSE)

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_importmessages')
    def admin_importmessages(self, lang, file, REQUEST=None, RESPONSE=None):
        """ """
        if REQUEST:
            if not file:
                self.getSite().setSessionErrorsTrans('You must select a file to import.')
                return REQUEST.RESPONSE.redirect('%s/admin_importexport_html' % self.absolute_url())
            else:
                self.manage_import_po(file, lang, REQUEST, RESPONSE)
                self.getSite().setSessionInfoTrans(MESSAGE_SAVEDCHANGES, date=self.utGetTodayDate())
                return REQUEST.RESPONSE.redirect('%s/admin_translations_html' % self.absolute_url())

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_exportxliff')
    def admin_exportxliff(self, x, export_all=1, REQUEST=None, RESPONSE=None):
        """ """
        return self.manage_export_xliff(export_all, x, REQUEST, RESPONSE)

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_importxliff')
    def admin_importxliff(self, file, REQUEST=None):
        """ """
        raise NotImplementedError("Imports not yet implemented in naaya.i18n")
        if REQUEST:
            if not file:
                self.getSite().setSessionErrorsTrans('You must select a file to import.')
                return REQUEST.RESPONSE.redirect('%s/admin_importexport_html' % self.absolute_url())
            else:
                self.manage_xliff_import(file)
                self.getSite().setSessionInfoTrans(MESSAGE_SAVEDCHANGES, date=self.utGetTodayDate())
                return REQUEST.RESPONSE.redirect('%s/admin_translations_html' % self.absolute_url())

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'spreadsheet_export')
    def spreadsheet_export(self, target_lang, dialect, REQUEST, RESPONSE):
        """ """
        cat = self.get_message_catalog()
        export_tool = self.get_importexport_tool()
        (headers, content) = export_tool.spreadsheet_export(target_lang, dialect)
        for (header_key, header_value) in headers:
            RESPONSE.setHeader(header_key, header_value)
        return content

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'spreadsheet_import')
    def spreadsheet_import(self, file, target_lang, dialect, REQUEST, RESPONSE):
        """ """
        if not file:
            self.setSessionErrorsTrans('You must select a file to import.')
            return RESPONSE.redirect('%s/admin_importexport_html' %
                                     self.absolute_url())
        cat = self.get_message_catalog()
        export_tool = self.get_importexport_tool()
        try:
            export_tool.spreadsheet_import(file, target_lang, dialect)
            self.setSessionInfoTrans('Translations successfully imported.')
        except KeyError, e:
            self.setSessionErrorsTrans('File format does not match selected format.')
        except UnicodeDecodeError, e:
            self.setSessionErrorsTrans('File needs to be utf-8 encoded.')
class NaayaI18n(Folder):
    """
    Naaya instantiates a **NaayaI18n** object insides its root. This object
    holds the whole internationalization data and operations:
    - management of languages
    - negotiation
    - translating and translation data
    - management of object property localization

    """

    meta_type = METATYPE_NAAYAI18N
    icon = 'misc_/portal_i18n/icon.gif'

    security = ClassSecurityInfo()

    def __init__(self, id, title, languages=[('en', 'English')]):
        self.id = id
        self.title = title
        n_languages = []
        for (code, name) in languages:
            n_languages.append((normalize_code(code), name))
        self._portal_langs = NyPortalLanguageManager(n_languages)
        lang_codes = tuple([x[0] for x in n_languages])
        catalog = NyMessageCatalog('translation_catalog',
                                   'Translation Catalog', lang_codes)
        self._catalog = catalog

    def get_negotiator(self):
        """ Return NyNegotiator instance, based on current request """
        try:
            return NyNegotiator(request=self.getSite().REQUEST)
        except AttributeError:
            # maybe debug/testing environment?
            return NyNegotiator()

    def get_message_catalog(self):
        """
        Returns Message Catalog (NyMessageCatalog instance).
        The Message Catalog stores translations for all messages.

        """
        return self._catalog

    def get_lang_manager(self):
        """
        Returns a NyLanguages instance, capable of managing
        a list of ISO 639 language code and names, based on languages.txt
        in naaya.i18n

        """
        if not hasattr(self, 'lang_manager'):
            self.lang_manager = NyLanguages()
        return self.lang_manager

    def get_portal_lang_manager(self):
        """
        Returns NyPortalLanguageManager instance in portal, capable
        of managing available languages

        """
        return self._portal_langs

    def get_importexport_tool(self):
        """ Returns the import-export wrapper for Message Catalog """
        return TranslationsImportExport(self.get_message_catalog())

    ### More specific methods:
    def get_language_name(self, code):
        """
        Get the language name for 'code'. It first looks up
        languages manually added in portal, then asks for languages
        in NyLanguages (naaya.i18n/languages.txt) which finally falls back
        to '???' string

        """
        if code in self.get_portal_lang_manager().getAvailableLanguages():
            # try to get name from added langs to site
            return self.get_portal_lang_manager().get_language_name(code)
        else:
            # not there, default to NyLanguages: loads the large languages.txt
            return self.get_lang_manager().get_language_name(code)

    security.declarePublic('get_languages_mapping')

    def get_languages_mapping(self):
        """
        Returns
        [{'code': 'xx', 'name': 'Xxx xx', default: True/False}, .. ]
        for languages currently available in portal

        """
        langs = list(self._portal_langs.getAvailableLanguages())
        langs.sort()
        result = []
        default = self._portal_langs.get_default_language()
        for l in langs:
            result.append({
                'code': l,
                'name': self.get_language_name(l),
                'default': l == default
            })
        return result

    def add_language(self, lang_code, lang_name=None):
        """
        Adds a new supported language in portal_i18n.
        Language code, language name can be any combination.
        If language name is not provided,
        a lookup is being performed in naaya.i18n/languages.txt

        """
        if not lang_code:
            raise ValueError('No language code provided')
        lang_code = normalize_code(lang_code)
        if lang_name is None:
            # search for name directly in languages.txt, obviously not in site
            lang_name = self.get_lang_manager().get_language_name(lang_code)
        # add language to portal:
        self._portal_langs.addAvailableLanguage(lang_code, lang_name)
        # and to catalog:
        self._catalog.add_language(lang_code)

    def del_language(self, lang):
        """
        Deletes a language from portal_i18n:
         * removes it from available languages
         * removes it from supported languages in message catalog

        """
        self._portal_langs.delAvailableLanguage(lang)
        self._catalog.del_language(lang)

    def get_selected_language(self, context=None):
        """ Performs negotiation and returns selected languag based on context
        or finds context using threading.local patch """
        return self.get_negotiator().getLanguage(
            self._portal_langs.getAvailableLanguages(), context)

    def change_selected_language(self, lang, goto=None, expires=None):
        """ Sets a cookie with a new preferred selected language """
        request = self.REQUEST
        response = request.RESPONSE
        negotiator = self.get_negotiator()

        # Changes the cookie (it could be something different)
        parent = self.aq_parent
        path = parent.absolute_url()[len(request['SERVER_URL']):] or '/'
        if expires is None:
            response.setCookie(negotiator.cookie_id, lang, path=path)
        else:
            response.setCookie(negotiator.cookie_id,
                               lang,
                               path=path,
                               expires=unquote(expires))
        # Comes back
        if goto is None:
            goto = request['HTTP_REFERER']

        response.redirect(goto)

    ### Public i18n related methods
    security.declarePublic('get_message_translation')

    def get_message_translation(self, message='', lang=None, default=None):
        """
        Returns the translation of the given message in the given language,
        as it is stored in Message Catalog (no interpolation).

        """
        if message == '':
            return None
        if not lang:
            lang = self.get_selected_language()
        return self.get_message_catalog().gettext(message, lang, default)

    security.declarePublic('get_translation')

    def get_translation(self, msg, **kwargs):
        """
        Translate message using Message Catalog
        and substitute named identifiers with values supplied by kwargs mapping

        """
        lang = self.get_selected_language()
        msg = self.get_message_catalog().gettext(msg, lang)
        return interpolate(msg, kwargs)

    ### Public general purpose methods
    security.declarePublic('message_decode')

    def message_decode(self, message):
        """
        Decodes a message from an ASCII string.

        To be used in the user interface, to avoid problems with the
        encodings, HTML entities, etc..

        """
        message = decodestring(message)
        encoding = HTTPRequest.default_encoding
        return unicode(message, encoding)

    security.declarePublic('message_decode')

    def message_encode(self, message):
        """
        Encodes a message to an ASCII string.

        To be used in the user interface, to avoid problems with the
        encodings, HTML entities, etc..

        """
        if isinstance(message, unicode):
            encoding = HTTPRequest.default_encoding
            message = message.encode(encoding)

        return encodestring(message)

    ### Private methods for private views

    security.declareProtected('Manage messages', 'manage_messages')

    def external_translate(self, message, target_lang):
        """
        Private method that returns a translation based on an external
        service, e.g. Google Translate. Not public because the number of
        requests must not be abusive

        """
        return external_translate(message, target_lang)

    #######################################################################
    # Management screens
    #######################################################################
    def manage_options(self):
        """ """
        options = (
            {'label': u'Messages', 'action': 'manage_messages'},
            {'label': u'Languages', 'action': 'manage_languages'},
            {'label': u'Import', 'action': 'manage_import'
            },
            {'label': u'Export', 'action': 'manage_export'
            }) \
            + SimpleItem.manage_options
        r = []
        for option in options:
            option = option.copy()
            r.append(option)
        return r

    #### Messages Tab ####

    security.declarePublic('get_namespace')

    def get_namespace(self, REQUEST):
        """For the management interface, allows to filter the messages to
        show.
        """
        # Check whether there are languages or not
        languages = self.get_languages_mapping()
        if not languages:
            return {}

        # Input
        batch_start = REQUEST.get('batch_start', 0)
        batch_size = REQUEST.get('batch_size', 15)
        empty = REQUEST.get('empty', 0)
        regex = REQUEST.get('regex', '')
        message = REQUEST.get('msg', None)

        # Build the namespace
        namespace = {}
        namespace['batch_size'] = batch_size
        namespace['empty'] = empty
        namespace['regex'] = regex

        # The language
        lang = REQUEST.get('lang', None) or languages[0]['code']
        namespace['language'] = lang

        # Filter the messages
        query = regex.strip()
        try:
            query = re.compile(query)
        except:
            query = re.compile('')

        messages = []
        for m, t in self.get_message_catalog().messages():
            if query.search(m) and (not empty or not t.get(lang, '').strip()):
                messages.append(m)
        messages.sort(filter_sort)
        # How many messages
        n = len(messages)
        namespace['n_messages'] = n

        # Calculate the start
        while batch_start >= n:
            batch_start = batch_start - batch_size
        if batch_start < 0:
            batch_start = 0
        namespace['batch_start'] = batch_start
        # Select the batch to show
        batch_end = batch_start + batch_size
        messages = messages[batch_start:batch_end]
        # Batch links
        namespace['previous'] = get_url(REQUEST.URL, batch_start - batch_size,
                                        batch_size, regex, lang, empty)
        namespace['next'] = get_url(REQUEST.URL, batch_start + batch_size,
                                    batch_size, regex, lang, empty)

        # Get the message
        message_encoded = None
        #translations = {}
        translation = None
        if message is None:
            if messages:
                message = messages[0]
                translation = self.get_message_catalog()\
                                  .gettext(message, lang, '')
                message = to_unicode(message)
                message_encoded = self.message_encode(message)
        else:
            message_encoded = message
            message = self.message_decode(message_encoded)
            #translations = self.get_translations(message)
            translation = self.get_message_catalog().gettext(message, lang, '')
            message = to_unicode(message)
        namespace['message'] = message
        namespace['message_encoded'] = message_encoded
        #namespace['translations'] = translations
        namespace['translation'] = translation

        # Calculate the current message
        namespace['messages'] = []
        for x in messages:
            x = to_unicode(x)
            x_encoded = self.message_encode(x)
            url = get_url(REQUEST.URL,
                          batch_start,
                          batch_size,
                          regex,
                          lang,
                          empty,
                          msg=x_encoded)
            namespace['messages'].append({
                'message': x,
                'message_encoded': x_encoded,
                'current': x == message,
                'url': url
            })

        # The languages
        for language in languages:
            code = language['code']
            language['name'] = _(language['name'])
            language['url'] = get_url(REQUEST.URL,
                                      batch_start,
                                      batch_size,
                                      regex,
                                      code,
                                      empty,
                                      msg=message_encoded)
        namespace['languages'] = languages

        return namespace

    security.declareProtected('Manage messages', 'manage_messages')
    manage_messages = DTMLFile('zpt/messages', globals())

    security.declareProtected('Manage messages', 'manage_editMessage')

    def manage_editMessage(self, message, language, translation, REQUEST,
                           RESPONSE):
        """Modifies a message.
        """
        message_encoded = message
        message_key = self.message_decode(message_encoded)
        self.get_message_catalog()\
            .edit_message(message_key, language, translation)

        url = get_url(REQUEST.URL1 + '/manage_messages',
                      REQUEST['batch_start'],
                      REQUEST['batch_size'],
                      REQUEST['regex'],
                      REQUEST.get('lang', ''),
                      REQUEST.get('empty', 0),
                      msg=message_encoded,
                      manage_tabs_message=_(u'Saved changes.'))
        RESPONSE.redirect(url)

    security.declareProtected('Manage messages', 'manage_delMessage')

    def manage_delMessage(self, message, REQUEST, RESPONSE):
        """ """
        message_key = self.message_decode(message)
        self.get_message_catalog().del_message(message_key)

        url = get_url(REQUEST.URL1 + '/manage_messages',
                      REQUEST['batch_start'],
                      REQUEST['batch_size'],
                      REQUEST['regex'],
                      REQUEST.get('lang', ''),
                      REQUEST.get('empty', 0),
                      manage_tabs_message=_(u'Saved changes.'))
        RESPONSE.redirect(url)

    #### Languages Tab ####

    security.declareProtected(view_management_screens, 'manage_languages')
    manage_languages = PageTemplateFile('zpt/languages', globals())

    security.declareProtected(view_management_screens, 'manage_addLanguage')

    def manage_addLanguage(self, language_code, language_name, REQUEST=None):
        """
        Add a new language for this portal.
        """
        self.getSite().gl_add_site_language(language_code, language_name)
        if REQUEST: REQUEST.RESPONSE.redirect('manage_languages?save=ok')

    security.declareProtected(view_management_screens, 'manage_delLanguages')

    def manage_delLanguages(self, languages, REQUEST=None):
        """
        Delete one or more languages.
        """
        self.getSite().gl_del_site_languages(languages)
        if REQUEST: REQUEST.RESPONSE.redirect('manage_languages?save=ok')

    security.declareProtected(view_management_screens,
                              'manage_changeDefaultLang')

    def manage_changeDefaultLang(self, language, REQUEST=None):
        """
        Change the default portal language.
        """
        self.getSite().gl_change_site_defaultlang(language)
        if REQUEST: REQUEST.RESPONSE.redirect('manage_languages?save=ok')

    #### Export Tab ####

    security.declareProtected(view_management_screens, 'manage_export')
    manage_export = PageTemplateFile('zpt/export_form', globals())

    security.declareProtected(view_management_screens, 'manage_export_po')

    def manage_export_po(self, language, REQUEST, RESPONSE):
        """ Provides pot/po export file for download """
        export_tool = self.get_importexport_tool()
        if language == 'locale.pot':
            filename = language
        else:
            filename = '%s.po' % language
        RESPONSE.setHeader('Content-type', 'application/data')
        RESPONSE.setHeader('Content-Disposition',
                           'inline;filename=%s' % filename)
        return export_tool.export_po(language)

    security.declareProtected(view_management_screens, 'manage_export_xliff')

    def manage_export_xliff(self, export_all, language, REQUEST, RESPONSE):
        """ Provides xliff file for download """
        fname = ('%s_%s.xlf' %
                 (self.get_message_catalog()._default_language, language))
        # Generate the XLIFF file header
        RESPONSE.setHeader('Content-Type', 'text/xml; charset=UTF-8')
        RESPONSE.setHeader('Content-Disposition',
                           'attachment; filename="%s"' % fname)
        export_tool = self.get_importexport_tool()
        return export_tool.export_xliff(language,
                                        export_all=bool(int(export_all)))

    security.declareProtected(view_management_screens, 'manage_export_tmx')

    def manage_export_tmx(self, REQUEST, RESPONSE):
        """ Provides tmx file for download """
        cat = self.get_message_catalog()
        fname = '%s.tmx' % cat.title
        export_tool = self.get_importexport_tool()
        header = export_tool.get_po_header(cat._default_language)
        charset = header['charset']
        # Generate the XLIFF file header
        RESPONSE.setHeader('Content-Type', 'text/xml; charset=%s' % charset)
        RESPONSE.setHeader('Content-Disposition',
                           'attachment; filename="%s"' % fname)
        return export_tool.export_tmx()

    #### Import Tab ####

    security.declareProtected(view_management_screens, 'manage_import')
    manage_import = PageTemplateFile('zpt/import_form', globals())

    security.declareProtected(view_management_screens, 'manage_import_po')

    def manage_import_po(self, file, language, REQUEST, RESPONSE):
        """ Import PO file into catalog, for an existing language """
        if language not in self.get_portal_lang_manager(
        ).getAvailableLanguages():
            raise ValueError('%s language is not available in portal' %
                             language)
        else:
            import_tool = self.get_importexport_tool()
            import_tool.import_po(language, file)
        RESPONSE.redirect('manage_import?save=ok')

    security.declareProtected(view_management_screens, 'manage_import_tmx')

    def manage_import_tmx(self, file, language, REQUEST, RESPONSE):
        raise NotImplementedError("Tmx import is not yet implemented")

    security.declareProtected(view_management_screens, 'manage_import_xliff')

    def manage_import_xliff(self, file, language, REQUEST, RESPONSE):
        raise NotImplementedError("Xliff import is not yet implemented")
Example #6
0
class NaayaI18n(SimpleItem):
    """
    Naaya instantiates a **NaayaI18n** object insides its root. This object
    holds the whole internationalization data and operations:
    - management of languages
    - negotiation
    - translating and translation data
    - management of object property localization

    """

    meta_type = METATYPE_NAAYAI18N
    icon = 'misc_/portal_i18n/icon.gif'

    security = ClassSecurityInfo()

    message_debug_list = ()
    message_debug_exception = False

    def __init__(self, id, title, languages=[('en', 'English')]):
        self.id = id
        self.title = title
        n_languages = []
        for (code, name) in languages:
            n_languages.append((normalize_code(code), name))
        self._portal_langs = NyPortalLanguageManager(n_languages)
        lang_codes = tuple([x[0] for x in n_languages])
        catalog = NyMessageCatalog('translation_catalog',
                                   'Translation Catalog', lang_codes)
        self._catalog = catalog

    ### Getters ###

    security.declarePrivate('get_negotiator')

    def get_negotiator(self):
        """ Return NyNegotiator instance, based on current request """
        return NyNegotiator()

    security.declarePrivate('get_message_catalog')

    def get_message_catalog(self):
        """
        Returns Message Catalog (NyMessageCatalog instance).
        The Message Catalog stores translations for all messages.

        """
        return self._catalog

    security.declarePrivate('get_lang_manager')

    def get_lang_manager(self):
        """
        Returns NyPortalLanguageManager instance in portal, capable
        of managing available languages

        """
        return self._portal_langs

    security.declarePrivate('get_importexport_tool')

    def get_importexport_tool(self):
        """ Returns the import-export wrapper for Message Catalog """
        return TranslationsImportExport(self.get_message_catalog())

    ### More specific language methods ###

    security.declarePublic('get_language_name')

    def get_language_name(self, code):
        """
        Get the language name for 'code'. First it looks up
        languages manually added in portal, then inquiries languages
        in naaya.i18n/languages.txt which finally falls back
        to '???' string

        """
        lang_manager = self.get_lang_manager()
        if code in lang_manager.getAvailableLanguages():
            # try to get name from added langs to site
            return lang_manager.get_language_name(code)
        else:
            # not there, default to languages.txt
            return get_iso639_name(code)

    security.declarePublic('get_languages_mapping')

    def get_languages_mapping(self):
        """
        Returns a list of mappings
        [ {'code': 'lang-code', 'name': 'Language name'}, .. ]
        containing languages currently available in portal.

        """
        langs = list(self._portal_langs.getAvailableLanguages())
        result = []
        default = self._portal_langs.get_default_language()
        for l in langs:
            result.append({
                'code': l,
                'name': self.get_language_name(l),
                'default': l == default
            })
        return result

    security.declarePrivate('add_language')

    def add_language(self, lang_code, lang_name=None):
        """
        Adds a new supported language in portal_i18n.
        Language code, language name can be any combination.
        If language name is not provided,
        a lookup is being performed in naaya.i18n/languages.txt

        """
        if not lang_code:
            raise ValueError('No language code provided')
        lang_code = normalize_code(lang_code)
        if lang_name is None:
            # search for name directly in languages.txt, obviously not in site
            lang_name = get_iso639_name(lang_code)
        # add language to portal:
        self._portal_langs.addAvailableLanguage(lang_code, lang_name)
        # and to catalog:
        self._catalog.add_language(lang_code)

    security.declarePrivate('del_language')

    def del_language(self, lang):
        """
        Deletes a language from portal_i18n:
         * removes it from available languages
         * removes it from supported languages in message catalog

        """
        self._portal_langs.delAvailableLanguage(lang)
        self._catalog.del_language(lang)

    security.declarePublic('get_selected_language')

    def get_selected_language(self, context=None):
        """
        Performs negotiation and returns selected language based on context
        or grabs context (request) by acquisition.

        """
        if context is None:
            context = self.getSite().REQUEST
        available_languages = self.get_lang_manager().getAvailableLanguages()
        return self.get_negotiator().getLanguage(available_languages, context)

    security.declarePublic('get_default_language')

    def get_default_language(self):
        """ Returns default language in portal """
        return self.get_lang_manager().get_default_language()

    security.declarePublic('change_selected_language')

    def change_selected_language(self, lang, goto=None):
        """ Sets a cookie with a new preferred selected language """
        request = self.REQUEST
        response = request.RESPONSE
        negotiator = self.get_negotiator()

        negotiator.change_language(lang, self, request)

        # Comes back
        if goto is None:
            goto = request['HTTP_REFERER']

        response.redirect(goto)

    ### Translation API ###

    security.declarePublic('get_translation')

    def get_translation(self, msg, _default=None, **kwargs):
        """
        Translate message in selected language using Message Catalog
        and substitute named identifiers with values supplied by kwargs mapping

        Most commonly used - a straight forward complete translation
        in selected language.

        """
        lang = kwargs.get('lang') or self.get_selected_language()
        msg = self.get_message_catalog().gettext(msg, lang, default=_default)
        return interpolate(msg, kwargs)

    ### Private methods for private views

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'external_translate')

    def external_translate(self, message, target_lang):
        """
        Private method that returns a translation based on an external
        service, e.g. Google Translate. Not public because the number of
        requests must not be abusive

        """
        return external_translate(message, target_lang)

    #######################################################################
    # Management screens
    #######################################################################
    def manage_options(self):
        """ """
        options = (
            {
                'label': u'Messages',
                'action': 'manage_messages'
            },
            {
                'label': u'Languages',
                'action': 'manage_languages'
            },
            {
                'label': u'Import',
                'action': 'manage_import'
            },
            {
                'label': u'Export',
                'action': 'manage_export'
            },
            {
                'label': u'Debug',
                'action': 'manage_debug'
            },
        ) + SimpleItem.manage_options
        r = []
        for option in options:
            option = option.copy()
            r.append(option)
        return r

    #### Messages Tab ####

    security.declarePublic('get_namespace')

    def get_namespace(self, REQUEST):
        """For the management interface, allows to filter the messages to
        show.
        """
        # Check whether there are languages or not
        languages = self.get_languages_mapping()
        if not languages:
            return {}

        # Input
        batch_start = REQUEST.get('batch_start', 0)
        batch_size = REQUEST.get('batch_size', 15)
        empty = REQUEST.get('empty', 0)
        regex = REQUEST.get('regex', '')
        message = REQUEST.get('msg', None)

        # Build the namespace
        namespace = {}
        namespace['batch_size'] = batch_size
        namespace['empty'] = empty
        namespace['regex'] = regex

        # The language
        lang = REQUEST.get('lang', None) or languages[0]['code']
        namespace['language'] = lang

        # Filter the messages
        query = regex.strip()
        try:
            query = re.compile(query)
        except:
            query = re.compile('')

        messages = []
        for m, t in self.get_message_catalog().messages():
            if query.search(m) and (not empty or not t.get(lang, '').strip()):
                messages.append(m)
        messages.sort()
        # How many messages
        n = len(messages)
        namespace['n_messages'] = n

        # Calculate the start
        while batch_start >= n:
            batch_start = batch_start - batch_size
        if batch_start < 0:
            batch_start = 0
        namespace['batch_start'] = batch_start
        # Select the batch to show
        batch_end = batch_start + batch_size
        messages = messages[batch_start:batch_end]
        # Batch links
        namespace['previous'] = get_url(REQUEST.URL, batch_start - batch_size,
                                        batch_size, regex, lang, empty)
        namespace['next'] = get_url(REQUEST.URL, batch_start + batch_size,
                                    batch_size, regex, lang, empty)

        # Get the message
        message_encoded = None
        #translations = {}
        translation = None
        if message is None:
            if messages:
                message = messages[0]
                translation = self.get_message_catalog()\
                                  .gettext(message, lang, '')
                message_encoded = message_encode(message)
        else:
            message_encoded = message
            message = message_decode(message_encoded)
            #translations = self.get_translations(message)
            translation = self.get_message_catalog().gettext(message, lang, '')
        namespace['message'] = message
        namespace['message_encoded'] = message_encoded
        #namespace['translations'] = translations
        namespace['translation'] = translation

        # Calculate the current message
        namespace['messages'] = []
        for x in messages:
            x_encoded = message_encode(x)
            url = get_url(REQUEST.URL,
                          batch_start,
                          batch_size,
                          regex,
                          lang,
                          empty,
                          msg=x_encoded)
            namespace['messages'].append({
                'message': x,
                'message_encoded': x_encoded,
                'current': x == message,
                'url': url
            })

        # The languages
        for language in languages:
            code = language['code']
            language['name'] = self.get_translation(unicode(language['name']))
            language['url'] = get_url(REQUEST.URL,
                                      batch_start,
                                      batch_size,
                                      regex,
                                      code,
                                      empty,
                                      msg=message_encoded)
        namespace['languages'] = languages

        return namespace

    security.declareProtected('Manage messages', 'manage_messages')
    manage_messages = DTMLFile('zpt/messages', globals())

    security.declareProtected('Manage messages', 'manage_editMessage')

    def manage_editMessage(self, message, language, translation, REQUEST,
                           RESPONSE):
        """Modifies a message.
        """
        message_encoded = message
        message_key = message_decode(message_encoded)
        self.get_message_catalog()\
            .edit_message(message_key, language, translation)

        url = get_url(
            REQUEST.URL1 + '/manage_messages',
            REQUEST['batch_start'],
            REQUEST['batch_size'],
            REQUEST['regex'],
            REQUEST.get('lang', ''),
            REQUEST.get('empty', 0),
            msg=message_encoded,
            manage_tabs_message=self.get_translation(u'Saved changes.'))
        RESPONSE.redirect(url)

    security.declareProtected('Manage messages', 'manage_delMessage')

    def manage_delMessage(self, message, REQUEST, RESPONSE):
        """Deletes a message in catalog"""
        message_key = message_decode(message)
        self.get_message_catalog().del_message(message_key)

        url = get_url(
            REQUEST.URL1 + '/manage_messages',
            REQUEST['batch_start'],
            REQUEST['batch_size'],
            REQUEST['regex'],
            REQUEST.get('lang', ''),
            REQUEST.get('empty', 0),
            manage_tabs_message=self.get_translation(u'Saved changes.'))
        RESPONSE.redirect(url)

    #### Languages Tab ####

    security.declareProtected(view_management_screens, 'manage_languages')
    manage_languages = PageTemplateFile('zpt/languages', globals())

    security.declareProtected(view_management_screens, 'manage_addLanguage')

    def manage_addLanguage(self, language_code, language_name, REQUEST=None):
        """
        Add a new language for this portal.
        """
        self.getSite().gl_add_site_language(language_code, language_name)
        if REQUEST is not None:
            REQUEST.RESPONSE.redirect('manage_languages?save=ok')

    security.declareProtected(view_management_screens, 'manage_delLanguages')

    def manage_delLanguages(self,
                            languages=[],
                            display_order='',
                            REQUEST=None):
        """
        Delete one or more languages. Also handles move up/down language
        in display order.

        """
        if languages:
            self.getSite().gl_del_site_languages(languages)
        if display_order:
            lang_manager = self.get_lang_manager()
            lang_manager.set_display_order(display_order)
        if REQUEST is not None:
            REQUEST.RESPONSE.redirect('manage_languages?save=ok')

    security.declareProtected(view_management_screens,
                              'manage_changeDefaultLang')

    def manage_changeDefaultLang(self, language, REQUEST=None):
        """
        Change the default portal language.
        """
        self.getSite().gl_change_site_defaultlang(language)
        if REQUEST is not None:
            REQUEST.RESPONSE.redirect('manage_languages?save=ok')

    #### Export Tab ####

    security.declareProtected(view_management_screens, 'manage_export')
    manage_export = PageTemplateFile('zpt/export_form', globals())

    security.declareProtected(view_management_screens, 'manage_export_po')

    def manage_export_po(self, language, REQUEST, RESPONSE):
        """ Provides pot/po export file for download """
        export_tool = self.get_importexport_tool()
        if language == 'locale.pot':
            filename = language
        else:
            filename = '%s.po' % language
        RESPONSE.setHeader('Content-type', 'application/data')
        RESPONSE.setHeader('Content-Disposition',
                           'inline;filename=%s' % filename)
        return export_tool.export_po(language)

    security.declareProtected(view_management_screens, 'manage_export_xliff')

    def manage_export_xliff(self, export_all, language, REQUEST, RESPONSE):
        """ Provides xliff file for download """
        fname = ('%s_%s.xlf' %
                 (self.get_message_catalog()._default_language, language))
        # Generate the XLIFF file header
        RESPONSE.setHeader('Content-Type', 'text/xml; charset=UTF-8')
        RESPONSE.setHeader('Content-Disposition',
                           'attachment; filename="%s"' % fname)
        export_tool = self.get_importexport_tool()
        return export_tool.export_xliff(language,
                                        export_all=bool(int(export_all)))

    security.declareProtected(view_management_screens, 'manage_export_tmx')

    def manage_export_tmx(self, REQUEST, RESPONSE):
        """ Provides tmx file for download """
        cat = self.get_message_catalog()
        fname = '%s.tmx' % cat.title
        export_tool = self.get_importexport_tool()
        header = export_tool.get_po_header(cat._default_language)
        charset = header['charset']
        # Generate the XLIFF file header
        RESPONSE.setHeader('Content-Type', 'text/xml; charset=%s' % charset)
        RESPONSE.setHeader('Content-Disposition',
                           'attachment; filename="%s"' % fname)
        return export_tool.export_tmx()

    #### Import Tab ####

    security.declareProtected(view_management_screens, 'manage_import')
    manage_import = PageTemplateFile('zpt/import_form', globals())

    security.declareProtected(view_management_screens, 'manage_import_po')

    def manage_import_po(self, file, language, REQUEST, RESPONSE):
        """ Import PO file into catalog, for an existing language """
        if language not in self.get_lang_manager().getAvailableLanguages():
            raise ValueError('%s language is not available in portal' %
                             language)
        else:
            import_tool = self.get_importexport_tool()
            import_tool.import_po(language, file)
        RESPONSE.redirect('manage_import?save=ok')

    security.declareProtected(view_management_screens, 'manage_import_tmx')

    def manage_import_tmx(self, file, language, REQUEST, RESPONSE):
        raise NotImplementedError("Tmx import is not yet implemented")

    security.declareProtected(view_management_screens, 'manage_import_xliff')

    def manage_import_xliff(self, file, language, REQUEST, RESPONSE):
        raise NotImplementedError("Xliff import is not yet implemented")

    _manage_debug = PageTemplateFile('zpt/debug', globals())
    security.declareProtected(view_management_screens, 'manage_debug')

    def manage_debug(self, REQUEST):
        """ Watch for problematic translation strings """

        if REQUEST.REQUEST_METHOD == 'POST':
            form = REQUEST.form
            self.message_debug_list = form['debug_strings'].splitlines()
            log.info('%s message_debug_list = %r', physical_path(self),
                     self.message_debug_list)
            self.message_debug_exception = bool(
                form.get('debug_exception', False))

            location = self.absolute_url() + '/manage_debug'
            return REQUEST.RESPONSE.redirect(location)

        return self._manage_debug(REQUEST)

    #######################################################################
    # Naaya Administration screens
    #######################################################################

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'get_admin_i18n')

    def get_admin_i18n(self):
        """
        Return a wrapper for portal_i18n, providing an interface used
        in rendering admin views and performing admin actions.

        """
        return AdminI18n(self).__of__(self)

    security.declareProtected(PERMISSION_TRANSLATE_PAGES,
                              'admin_translations_html')
    admin_translations_html = PageTemplateFile('zpt/site_admin_translations',
                                               globals())

    security.declareProtected(PERMISSION_TRANSLATE_PAGES,
                              'admin_messages_html')
    admin_messages_html = PageTemplateFile('zpt/site_admin_messages',
                                           globals())

    security.declareProtected(PERMISSION_TRANSLATE_PAGES,
                              'admin_importexport_html')
    admin_importexport_html = PageTemplateFile('zpt/site_admin_importexport',
                                               globals())

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_editmessage')

    def admin_editmessage(self,
                          message,
                          language,
                          translation,
                          start,
                          skey,
                          rkey,
                          query,
                          REQUEST=None):
        """ """
        ob = self.get_message_catalog()
        ob.edit_message(message_decode(message), language, translation)
        if REQUEST:
            self.setSessionInfoTrans(MESSAGE_SAVEDCHANGES,
                                     date=self.utGetTodayDate())
            REQUEST.RESPONSE.redirect('%s/admin_messages_html?msg=%s&start=%s&skey=%s&rkey=%s&query=%s&trans_lang=%s&saved=True' % \
                (self.absolute_url(), quote(message), start, skey, rkey, query, language))

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_delmesg_html')
    admin_delmesg_html = PageTemplateFile('zpt/site_admin_delmessages',
                                          globals())

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_delmsg')

    def admin_delmsg(self, messages=[], REQUEST=None):
        """ """
        message_catalog = self.get_message_catalog()
        messages = self.utConvertToList(messages)
        for message in messages:
            message_catalog.del_message(message)
        if REQUEST:
            self.getSite().setSessionInfoTrans(MESSAGE_SAVEDCHANGES,
                                               date=self.utGetTodayDate())
            REQUEST.RESPONSE.redirect('%s/admin_delmesg_html' %
                                      self.absolute_url())

    security.declareProtected(PERMISSION_PUBLISH_OBJECTS,
                              'admin_basket_translations_html')
    admin_basket_translations_html = PageTemplateFile(
        'zpt/site_admin_basket_translations', globals())

    security.declareProtected(PERMISSION_TRANSLATE_PAGES,
                              'admin_exportmessages')

    def admin_exportmessages(self, x, REQUEST=None, RESPONSE=None):
        """ """
        return self.manage_export_po(x, REQUEST, RESPONSE)

    security.declareProtected(PERMISSION_TRANSLATE_PAGES,
                              'admin_importmessages')

    def admin_importmessages(self, lang, file, REQUEST=None, RESPONSE=None):
        """ """
        if REQUEST:
            if not file:
                self.getSite().setSessionErrorsTrans(
                    'You must select a file to import.')
                return REQUEST.RESPONSE.redirect('%s/admin_importexport_html' %
                                                 self.absolute_url())
            else:
                self.manage_import_po(file, lang, REQUEST, RESPONSE)
                self.getSite().setSessionInfoTrans(MESSAGE_SAVEDCHANGES,
                                                   date=self.utGetTodayDate())
                return REQUEST.RESPONSE.redirect('%s/admin_translations_html' %
                                                 self.absolute_url())

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_exportxliff')

    def admin_exportxliff(self, x, export_all=1, REQUEST=None, RESPONSE=None):
        """ """
        return self.manage_export_xliff(export_all, x, REQUEST, RESPONSE)

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'admin_importxliff')

    def admin_importxliff(self, file, REQUEST=None):
        """ """
        raise NotImplementedError("Imports not yet implemented in naaya.i18n")
        if REQUEST:
            if not file:
                self.getSite().setSessionErrorsTrans(
                    'You must select a file to import.')
                return REQUEST.RESPONSE.redirect('%s/admin_importexport_html' %
                                                 self.absolute_url())
            else:
                self.manage_xliff_import(file)
                self.getSite().setSessionInfoTrans(MESSAGE_SAVEDCHANGES,
                                                   date=self.utGetTodayDate())
                return REQUEST.RESPONSE.redirect('%s/admin_translations_html' %
                                                 self.absolute_url())

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'spreadsheet_export')

    def spreadsheet_export(self, target_lang, dialect, REQUEST, RESPONSE):
        """ """
        cat = self.get_message_catalog()
        export_tool = self.get_importexport_tool()
        (headers,
         content) = export_tool.spreadsheet_export(target_lang, dialect)
        for (header_key, header_value) in headers:
            RESPONSE.setHeader(header_key, header_value)
        return content

    security.declareProtected(PERMISSION_TRANSLATE_PAGES, 'spreadsheet_import')

    def spreadsheet_import(self, file, target_lang, dialect, REQUEST,
                           RESPONSE):
        """ """
        if not file:
            self.setSessionErrorsTrans('You must select a file to import.')
            return RESPONSE.redirect('%s/admin_importexport_html' %
                                     self.absolute_url())
        cat = self.get_message_catalog()
        export_tool = self.get_importexport_tool()
        try:
            export_tool.spreadsheet_import(file, target_lang, dialect)
            self.setSessionInfoTrans('Translations successfully imported.')
        except KeyError, e:
            self.setSessionErrorsTrans(
                'File format does not match selected format.')
        except UnicodeDecodeError, e:
            self.setSessionErrorsTrans('File needs to be utf-8 encoded.')