Django ugettext_lazy, interpolation and ChoiceField

I need a ChoiceField with these options:

choices = [(1, '1 thing'), (2, '2 things'), (3, '3 things'), ...] 

and I want him to be translated.

This does not work:

 choices = [(i, ungettext_lazy('%s thing', '%s things', i) % i) for i in range(1,4)] 

because as soon as the lazy object is interpolated, it becomes a unicode object - since ChoiceField.choices is evaluated at startup, its selection will be in the language that was active at the start of Django.

I could use ugettext_lazy('%s things' % i) , but that would require a translation for each digit, which is stupid. What is the right way to do this?

+7
source share
3 answers

In the Django Translation documentation ... Working with lazy translation objects , I see a remark that seems to be affecting your problem here.

Using ugettext_lazy() and ungettext_lazy() so that String Values ​​in models and utility functions is a common operation. When you work with these objects elsewhere in your code, you must make sure that you do not accidentally convert them to strings, because they must be converted as late as possible (so that the correct locale is valid). This requires the use of an auxiliary function, described below.

They then introduce django.utils.functional.lazy(func, *resultclasses) , which is currently not covered by the django.utils.functional documentation. However, according to the django.utils.functional.py source code , it "turns any called into a lazy evaluated call .... the function is evaluated every time it is accessed."

By changing their example from other uses of the lazy in delayed translations to include your code, the following code may work for you.

 from django.utils import six # Python 3 compatibility from django.utils.functional import lazy from django.utils.safestring import mark_safe choices = [ (i, lazy( mark_safe(ungettext_lazy('%s thing', '%s things', i) % i), six.text_type )# lazy() for i in range(1,4) ] 

In addition, the django.utils.functional documentation mentions the django.utils.functional function allow_lazy(func, *resultclasses) . This allows you to write your own function, which takes a lazy string as arguments. "He changes the function so that if it is called with a lazy translation as the first argument, the evaluation of the function is delayed until it is converted to a string." lazy(func, *resultclasses) not a decorator, it changes the called one.

NB I have not tried this code in Django. I just pass what I found in the documentation. Hope it shows you what you can use.

+4
source

For those who are faced with this issue. Unfortunately, @Jim DeLaHunt's answer does not work completely - it almost is, but not quite what needs to be done.

Important differences are:

  • What you need to warp with lazy is a function that will return a text value and not another lazy translation object, or you will most likely see a strange <django.utils.functional.__proxy__ at ...> instead of the actual text ( IIRC Django will not delve into a chain of lazy objects). So use ungettext , not ungettext_lazy .

  • You want to perform string interpolation only when the completed function is executed. If you write lazy(f("%d" % 42)) , the interpolation will happen too soon - in this case, Python will look forward. And don't forget about variable areas - you can't just refer to an iterator from a wrapped function.

Here I used lambda , which receives the argument number and performs interpolation. The code inside lambda is only executed when parsing a lazy object, i.e. when displaying a selection.

So the working code is:

 choices = [ ( (i, lazy( lambda cnt: ungettext(u"%(count)d thing", u"%(count)d things", cnt) % {"count": cnt}, six.text_type )(i)) ) for i in [1, 2, 3] ] 

This will correctly have the same intended effect as

 choices = [ (1, _("1 thing")), (2, _("2 things")), (3, _("3 things")), ] 

But in the translation database there will be only one record, not several.

+2
source

This is similar to a situation where you could take advantage of the trick that Ilian Iliev blog deals with, Django forms a ChoiceField with dynamic values ​​....

Iliev shows a very similar initializer:

 my_choice_field = forms.ChoiceField(choices=get_my_choices()) 

He says: "The trick is that in this case, my_choice_field parameters are initialized when the server starts (re). Or, in other words, after the server starts, the selection is loaded (calculated) and they will not change until the next (re) Start." It looks like the same difficulty you are facing.

His trick: “Fortunately, the form class has an init method that is called each time the form is loaded. In most cases, you skipped it in the form definition, but now you have to use it.”

Here is his sample code mixed with a generator expression:

 class MyForm(forms.Form): def __init__(self, *args, **kwargs): super(MyForm, self).__init__(*args, **kwargs) self.fields['my_choice_field'] = forms.ChoiceField( choices=( (i, ungettext_lazy('%s thing', '%s things', i) % i) for i in range(1,4) )# choices= )# __init__ 

The generator expression is enclosed in parentheses so that it is treated as a generator object to which choices assigned.

NB I have not tried this code in Django. I just convey the idea of ​​Iliev.

0
source

All Articles