Search code examples
python-3.xtornado

make_lazy_gettext with wForms getting output .lazy_gettext at 0x07808D68>


I have application on python 3.5 that use tornado 4.5.1, i got in trouble with custom errors given in wForms, i did a research and wForms says that have to be used lazy_gettex in order to be able to translate custom errors passed to wForm Class.

I found the module speaklater that have make_lazy_gettext , but using it i gett in another trouble, instead of the error message on the front end i get something like this .lazy_gettext at 0x07808D68>

My code handler: hanlders.py inside the handler i have available self._ for gettext translations that now they work

class ChangePassword(ProfileHandler):
    def initialize(self, *args, **kwargs):
        super(ChangePassword, self).initialize(*args, **kwargs)
        self.form = profile_f.ChangePasswordForm(meta=self.meta_locale)

    @authenticated(domain='c')
    @coroutine
    def get(self):
        """::access::
        <div class="___sort">Profile</div>
        <div>
            <font class="__get__">GET</font>
            <i class="icon-arrow-right14"></i>
            <font class="desc">Change password</font>
        </div>
        ::
        ::auto_access::1::
        """
        response = yield self.model.item(_id=self.current_user)
        profile = response.get('user', {})
        response = yield self.quiz_m.get_all_quizzes(self.current_user)
        quizzes = response.get('quizzes', {})
        render_vars = {
            'form': self.form,
            'profile': profile,
            'quizzes': quizzes,
        }
        self.render('pages/profile/profile_edit_password.html', **render_vars)

    @authenticated(domain='c')
    @coroutine
    def post(self):
        """::access::
        <div class="___sort">Profile</div>
        <div>
            <font class="__post__">POST</font>
            <i class="icon-arrow-right14"></i>
            <font class="desc">Change password</font>
        </div>
        ::
        ::auto_access::1::
        """
        self.form.process(self.request.arguments)
        if self.form.validate():
            form_data = self.form.prepared_data
            update_response = yield self.model.update_password(
                self.current_user, new_password=form_data['new_password'], old_password=form_data['old_password']) # noqa
            if update_response['success']:
                # start send email
                user = yield self.model.mongo_find_one('users', ObjectId(self.current_user))
                render_vars = {
                    'user': user,
                    'new_password': form_data['new_password'],
                }
                yield emails_template.send_template(self, 'profile_password', render_vars, user['email'])
                # end send email
                self.redirect('/profile/')
                return
            else:
                self.form.set_foreign_errors({'old_password': update_response['errors']})
        response = yield self.model.item(_id=self.current_user)
        profile = response.get('user', {})
        response = yield self.quiz_m.get_all_quizzes(self.current_user)
        quizzes = response.get('quizzes', {})
        render_vars = {
            'form': self.form,
            'profile': profile,
            'quizzes': quizzes,
        }
        self.render('pages/profile/profile_edit_password.html', **render_vars)

My code below: forms.py

from copy import copy
from forms import BaseForm, vl, fl, CustomStringField
from forms import EmailValidator

from speaklater import make_lazy_gettext as _


class ChangePasswordForm(BaseForm):
    old_password = fl.PasswordField(validators=[vl.DataRequired(), vl.length(max=32, min=6)])
    new_password = fl.PasswordField(validators=[vl.DataRequired(), vl.length(max=32, min=6)])
    new_repassword = fl.PasswordField(validators=[vl.DataRequired(), vl.length(max=32, min=6)])

    def validate_new_repassword(self, field):
        if self.data.get('new_password') != field.data:
            raise vl.ValidationError(_('Incorrect retyped password'))

From the above code my custom error message is _('Incorrect retyped password') and the front end output i get this .lazy_gettext at 0x07808D68>

Any recommendation why this is happening?

Function for simple gettext used in application

def load_gettext_translations(dir_dom):
    # Todo: think about workaround
    """
    Like standard tornado.locale.load_gettext_translations but loads more locales from different locations
    :param dir_dom: {'directory': '', 'domain': '', 'prefix': ''}
    """
    import gettext
    # global _translations
    # global _supported_locales
    # global _use_gettext
    # _translations = {}
    for trans in dir_dom:
        for lang in os.listdir(trans['directory']):
            if lang.startswith('.'):
                continue  # skip .svn, etc
            if os.path.isfile(os.path.join(trans['directory'], lang)):
                continue
            try:
                os.stat(os.path.join(trans['directory'], lang, "LC_MESSAGES", trans['domain'] + ".mo"))
                tornado.locale._translations['%s%s' % (trans.get('prefix', ''), lang)] = gettext.translation(
                    trans['domain'], trans['directory'],
                    languages=[lang])
            except Exception as e:
                gen_log.error("Cannot load translation for '%s': %s", lang, str(e))
                continue
    tornado.locale._supported_locales = frozenset(list(tornado.locale._translations.keys()) + [tornado.locale._default_locale])
    tornado.locale._use_gettext = True
    gen_log.debug("Supported locales: %s", sorted(tornado.locale._supported_locales))

Solution

  • UPDATE: I've created a github repo as a demo for this answer. It can be found here: https://github.com/bhch/tornado-localization.


    You are using it wrong. From the speaklater docs make_lazy_gettext, as its name suggests, makes a lazy gettext object. It does not return a string object, instead it returns a function. So, when you're doing:

    msg = _('Message')
    
    msg is in fact a function, not a lazy string
    

    Correct usage will be like this:

    from speaklater import make_lazy_gettext
    
    
    def translate(message):
        """Function responsible for translating message"""
    
        return message # return message for the sake of example
    
    
    _ = make_lazy_gettext(lambda: translate)
    
    msg = _('Hello')
    
    print(msg)
    # outputs Hello
    

    The translate function is responsible for translations. I've used a simple function just to demonstrate the use of speaklater. But in a real application it must be able to translate the given string depending on the language of the client.


    How translation works:

    It seems that you think using _ or gettext automatically translates some message to any language.

    No. That is not how translations work.

    You will have to manually provide translations to all the messages you want to translate.

    In the translate() function in the example above, you can implement translations for your messages by creating dictionaries like :

    # spanish
    {
      'es_ES': {'Incorrect retyped password': 'Contraseña reescrita incorrecta'}
      # do this for other languages you want to support
    }
    

    And write the appropriate code in the translate() function which will look for the translation in this dictionary according to the language (locale) and return the translated message.


    If you want to provide support for many languages, writing dictionaries can be tiresome and rather impractical. For that purpose you can check out tornado-babel library to make things easier for you. Tornado also has support for locale. But that would be a little more work than using tornado-babel.


    Sample Tornado application

    One thing I should point out is that you can't use a global _ object because Tornado is single threaded. You will need to make _ object inside your handlers so that the locale stays different for each user.

    from tornado import web, locale
    from speaklater import make_lazy_gettext
    
    class MyHandler(web.RequestHandler):
        def get(self):
            # tornado handlers have a `self.locale` attribute
            # which returns the current user's locale
            _ = make_lazy_gettext(lambda: self.locale.translate)
    
            hello = _('hello')
            # do something else
    
    
    if __name__ == '__main__':
        # load the translation files
        locale.load_gettext_translations('/path/to/locale/', 'domain')
        # ...
    

    You will also need to modify the __init__ function of your ChangePasswordForm to accept the gettext object _.