Making verbose_name(_plural) more i18n-friendly (ticket #11688)

117 views
Skip to first unread message

Ramiro Morales

unread,
Nov 28, 2011, 5:49:26 AM11/28/11
to Djang...@googlegroups.com
Hi fellow translators,

I've been working that new-feature ticket starting with an idea Jannis
Leidel had posted in a GitHb [0]Gist some months ago and would like to
get some feedback from developers with real i18n experience about this
first iteration of a [1]patch for it.

Solving this would allow Django to support in a better way translation
of model names to a wider range of locales and could help with ticket
[2]#14844 by providing a base infrastructure for fixing it.

A brief description of the user-facing part of the proposed
implementation:

NOTE: The names are up for replacement in case they are too ugly too
generic or too prone to clash with existing code. I myself have renamed
them twice already (fortunately it is mostly a mass search and replace
operation).

It is composed of two parts:

Part 1:
=======

A new, optional

@classmethod
def verbose_names(cls, count=1)

that can be implemented by the programmer in her model. It is in charge
of returning the verbose name corresponding to /count/ model instances.

This means:

* The now deprecated Meta.verbose_name and Meta.verbose_name_plural
options have been promoted from the Meta inner class to the model
scope itself.

* Singular and plural forms treatment is unified by delegating in this
single method the job of returning them based on the value of
the /count/ parameter. Plural forms aren't limited to two (singular
case plus one plural form) as in English and most western European
languages.

Simple example for audience group #1: Authors of apps whose users use
and will always use one locale (e.g. English) so they set USE_I18N=False
and don't need to care for i18n::

1 class DjangoMascot(models.Model):
2 ...
3
4 @classmethod
5 def verbose_names(cls, count=1):
6 if count == 1:
7 return 'flying pony'
8 else:
9 return 'flying ponies'

verbose_names() can ignore one or more values of /count/ and implement
returning a valid verbose name only for certain values of such argument.
In that case Django will make sure default fallback values are provided
to consumers (see description of the public API in Part 2 and backward
compatibility sections below), e.g.::

1 class Poll(models.Model):
2 ...
3
4 # Developer didn't provide a verbose_names() classmethod.
5 # Django provided fallbacks means 'poll' and 'polls'
6 # will be returned when Poll.get_verbose_name(1) and
7 # Poll.get_verbose_name(<any values different from 1>) are
8 # called, respectively.

Other example::

1 class FirewallPolicy(models.Model):
2 ...
3
4 @classmethod
5 def verbose_names(cls, count=1):
6 if count != 1:
7 return 'firewall policies'
8 # Django provided fallback means 'firewall policy'
9 # will be returned when FirewallPolicy.get_verbose_name(1)
10 # is called

Example for audience group #2: Authors of i18n-aware apps and that
because of that use USE_I18N=True and functions from
django.utils.translation:

What currently is expressed in this fashion::

1 from django.utils.translation import ugettext_lazy as _
2
3 class Library(models.Model):
4 city_name = models.CharField(max_length=30)
5
6 class Meta
7 verbose_name = _('llbrary')
8 verbose_name_plural = _('llbraries')

Will need to be converted to this::

1 from django.utils.translation import ungettext_lazy
2
3 class Library(models.Model):
4 city_name = models.CharField(max_length=30)
5
6 @classmethod
7 def verbose_names(cls, count=1):
8 return ungettext_lazy('llbrary', 'libraries', count)

The fact that we can use ungettext_lazy() means that the gettext PO
catalog entries for this model will change from::

#: foo/models.py:7
msgid "library"
msgstr "<translation of 'library'>"

#: foo/models.py:8
msgid "libraries"
msgstr "<translation of 'libraries'>"

To this::

#: foo/models.py:8
msgid "library"
msgid_plural "libraries"
msgstr[0] "<translation of 'library' with plural form #1>"
...
msgstr[n] "<translation of 'library' with plural form #n+1>"

This is semantically more complete and will allow locales with more than
two plural forms to have better translations of phrases that involve
arbitrary quantities of Django models. E.g. Polish (pl) has three plural
forms and Irish (ga) has five.

Part 2:
=======

A django.db.Model.get_verbose_name(cls, count=1): classmethod that
provides a published API to obtain different flavors of verbose names of
the model at hand.

It is always available so it is always possible to get verbose
names of any model. This method isolates code that needs to get human
readable representations of model names from the particularities of
the verbose_names() model method described in Part 1, if any.

This method will seldomly be overridden by developers if at all. It's
implemented in the base Model class and contains the logic for backward
compatibility and to cope with the possible behaviors of user-provided
verbose_names().

All internal uses in Django itself have been converted to use this API.
Previously this was implemented by reading
<Model class>._meta.verbose_name and
<Model class>._meta.verbose_name_plural

Backward compatibility
----------------------

Backward compatibility has been implemented in a way such that:

o The presence of a verbose_names() classmethod in a model definition
has precedence over the Meta.verbose_name and
Meta.verbose_name_plural options

o Also, these options get their values from internal calls to this
method to preserve compatibility with other apps/code that will keep
using them during a transition period.

o The fallback get_verbose_name() return values in case the developer
doesn't implement verbose_names() (or implements it partially) in a
given model are the same Django has us accustomed to when we didn't
specify Meta.verbose_name and/or Meta.verbose_name_plural i.e.:

* CamelCase -> 'camel case' transformation for the singular case

* Singular verbose name + 's' for the plural case.

Deprecation process
-------------------

As implemented in the patch, currently PendingDeprecationWarning is
raised (one needs to specify the -Wall command line switch to the Python
interpreter to see them) when:

* Meta.verbose_name or Meta.verbose_name_plural are specified
in a model definition

* Model._meta.verbose_name or Model._meta.verbose_name_plural
are accessed (read) (implementation note: They've been converted
into properties)

In Django 1.4+1 the warnings will be changed to DeprecationWarning

Starting with Django 1.4+2 Meta.verbose_name and
Meta.verbose_name_plural will be ignored.

(Open) questions/Things that could be changed
---------------------------------------------

* Is it ok to add (class)method's to Model?, there is the possibility
of name clash with equally named existing attributes/method names in
user models.

* Should we make verbose_names() count argument not have a default value
(=1) and so make it mandatory to specify a value of model quantity?

* Why are these classmethods? Because there isn't anything in these
methods that might make them depend on a given model instance
characteristics to do their job and because many uses from other parts
of Django don't have a model instance at hand but rather the model
class.

* Further, maybe actually there is even no need to them be classmethods
and we can make them staticmethods?

* Can we drop the setter in the now properties _meta Option verbose_name
and verbose_name_plural? This would effectively convert them in
read-only options because we never officially supported modification
of these options at runtime.

Patch status
------------

Patch is in good shape, has documentation changes and tests although
I plan to expand them more.

Any kind of feedback is welcome!


0. https://gist.github.com/2000f763e15c260c0666
1. https://code.djangoproject.com/attachment/ticket/11688/11688-verbose_name_plural_evolution-1.diff
2. https://code.djangoproject.com/ticket/14844

Regards,

--
Ramiro Morales

Vladimir Macek

unread,
Nov 28, 2011, 7:36:58 AM11/28/11
to djang...@googlegroups.com
On 28.11.2011 11:49, Ramiro Morales wrote:
> Any kind of feedback is welcome!

Wonderful! Thanks for making verbose_names's really plural friendly!

--
: Vladimir Macek : http://macek.sandbox.cz : +420 608 978 164
: UNIX && Dev || Training : Python, Django : GPG key 1F059424

Sergiy Kuzmenko

unread,
Nov 28, 2011, 7:45:22 PM11/28/11
to djang...@googlegroups.com
My two cents:

1. Plural translation should output a fully translated phrase, e.g.:
ungettext('%(count)d poll', '%(count)d polls, count) % {'count': count, }
instead of noun alone, as in:
ungettext_lazy('poll', 'polls', count)

The advantage is that we provide translators with a complete translation string and thus there is no need to do stitching in the template with a specific word order in mind. Numeral + noun is not the universally acceptable word order [1].


2. I'm not very clear why we need both verbose_names and get_verbose_name. If get_verbose_name is going to be the published interface we probably don't need additional class methods, do we?



[1] http://wals.info/chapter/89


Thanks


--
You received this message because you are subscribed to the Google Groups "Django internationalization and localization" group.
To post to this group, send email to djang...@googlegroups.com.
To unsubscribe from this group, send email to django-i18n...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/django-i18n?hl=en.


Ramiro Morales

unread,
Nov 30, 2011, 10:41:55 AM11/30/11
to djang...@googlegroups.com
On Mon, Nov 28, 2011 at 9:45 PM, Sergiy Kuzmenko <s.kuz...@gmail.com> wrote:
> My two cents:
>
> 1. Plural translation should output a fully translated phrase, e.g.:
> ungettext('%(count)d poll', '%(count)d polls, count) % {'count': count, }
> instead of noun alone, as in:
> ungettext_lazy('poll', 'polls', count)
>

I think we all agree with this, i.e having full phrases being marked up
for translation is better than composing phrases with translated
fragments.

Problem is that the replacement for `Meta.verbose_name` and
`Meta.verbose_name_plural` we finally come up with also needs to:

* Maintain backward compatibility

* Not change verbose names behavior when i18n isn't being used. IMHO
disrupting the workflow native English developers (especially
English-speaking Django core developers) use (and the results
they get) the least possible this is particularly important to
increase the chances of a proposed fix being finally committed.)

* Simply accept the fact that bare human readable model verbose
names are also needed and very useful in many contexts. I believe that
if for some UI work one needs the 'Poll' literal (or its translation)
and instead one gets '1 Poll' it would be surprising to say the
least, not matter what locale is in effect.

So, making the implementation to always execute something like

ungettext('%(count)d poll', '%(count)d polls, count) % {'count': count, }

isn't the right thing to do either. At least with this particular API.

By the way, there is a piece of code in the admin app that after the patch
looks like this (`changecount` is the int variable that contains the number of
model instances involved)::

name = force_unicode(model.get_verbose_name(changecount))
msg = ungettext("%(count)s %(name)s was changed successfully.",
"%(count)s %(name)s were changed successfully.",
changecount) % {'count': changecount,
'name': name}
self.message_user(request, msg)

Here, we aren't using full phrase translation but we still are setting
things up in way so a) Translators can reorder the words, they just need
to keep in mind that the 'name' var will contain the right verbose name
for the plural form in use and b) Final users are shown correct model
verbose names in all locales.

(Having said this, I have been doing some work to see how a fix for
[1]#14844 -- "Getting the ``blocktrans` template tag to select the right
plural model verbose name for every locale" would look like, based on
the work for this ticket and at the same time to validate the proposed
implementation. Will attach my WIP patch to that ticket and post a
follow-up email to this thread with some further findings and thoughts
because they are related to this kind of problems.)

> The advantage is that we provide translators with a complete translation
> string and thus there is no need to do stitching in the template with a
> specific word order in mind. Numeral + noun is not the universally
> acceptable word order [1].
>

> [1] http://wals.info/chapter/89

Wow that links is a very good resource, thanks for sharing it.

> 2. I'm not very clear why we need both verbose_names and get_verbose_name.
> If get_verbose_name is going to be the published interface we probably don't
> need additional class methods, do we?

This is also related to the three constraints I mentioned above.

In fact my initial plan (reflected in the first patch) was to make
get_verbose_name() effectively a published interface but for consumers
that need model verbose name literals.

We need it to always exist because we need that it always returns
something more or less sane (be it what the model developer implemented
with the non-mandatory verbose_names() method or one of the backward
compatibility fallbacks)

(Remember that the developer that creates a model has the freedom to
provide or not a verbose_names() method, and to make it return
values for any subset of the plural forms. This freedom is needed to
mirror the one the developer has when using Meta.verbose_name and
Meta.verbose_name_plural.)

But in further discussion with Jannis Leidel via IRC we decided to not
leave get_verbose_name() at the model level and make it a method of the
still non-public inner `_meta` Options instance (possibly postponing its
publication until `_meta` gets officially documented). So, code that
needs to consume model verbose names need to stop using
`<Model class>._name._verbose_name*` and call `<Model
class>._name.get_verbose_name()`
instead.

I've changed this in a [2]second version of the patch I've just attached
to the ticket.

Thanks for reviewing and commenting.

1. https://code.djangoproject.com/ticket/14844
2. https://code.djangoproject.com/attachment/ticket/11688/11688-verbose_name_plural_evolution-2.diff

--
Ramiro Morales

Sergiy Kuzmenko

unread,
Nov 30, 2011, 10:48:35 PM11/30/11
to djang...@googlegroups.com
Problem is that the replacement for `Meta.verbose_name` and
`Meta.verbose_name_plural` we finally come up with also needs to:

* Maintain backward compatibility

* Not change verbose names behavior when i18n isn't being used. IMHO
 disrupting the workflow native English developers (especially
 English-speaking Django core developers) use (and the results
 they get) the least possible this is particularly important to
 increase the chances of a proposed fix being finally committed.)

* Simply accept the fact that bare human readable model verbose
 names are also needed and very useful in many contexts. I believe that
 if for some UI work one needs the 'Poll' literal (or its translation)
 and instead one gets '1 Poll' it would be surprising to say the
 least, not matter what locale is in effect.

So, making the implementation to always execute something like

 ungettext('%(count)d poll', '%(count)d polls, count) % {'count': count, }

isn't the right thing to do either. At least with this particular API.

These are good and valid points to which I agree. However, in the context of software localization there are two distinct usages for plural:

1. Unquantified plurals (i.e. some unknown qty greater than 1). This is used in model listing on the admin site landing page.
2. Quantified plurals (e.g., 4 cows). This is used in admin messages and admin paginator.

For languages with multiple plural forms, quantified plural without quantifier is rather meaningless.

With this in mind maybe we can leave Meta.verbose_name_plural as is for the sake of backward compatibility as well as for getting unquantified plural form of the model. And introduce a new method for getting quantified plurals only as I outlined earlier:


ungettext('%(count)d poll', '%(count)d polls, count) % {'count': count, }

(Unless there is an additional reason to get rid of Meta options.)
 
By the way, there is a piece of code in the admin app that after the patch
looks like this (`changecount` is the int variable that contains the number of
model instances involved)::

   name = force_unicode(model.get_verbose_name(changecount))
   msg = ungettext("%(count)s %(name)s was changed successfully.",
                   "%(count)s %(name)s were changed successfully.",
                   changecount) % {'count': changecount,
                                   'name': name}
   self.message_user(request, msg)
Here, we aren't using full phrase translation but we still are setting
things up in way so a) Translators can reorder the words, they just need
to keep in mind that the 'name' var will contain the right verbose name
for the plural form in use and b) Final users are shown correct model
verbose names in all locales.

The problem with current implementation: it does not work well for languages with multiple plural form because the form of %(name)s depends on the count. The proposed change will resolve this problem. But to achieve a correct plural translation we essentially need 2 translation call:

1) to get the correct plural form the model based on the quantifier
2) to get the resulting translation into "%(count)s %(name)s" string which I think can be somewhat confusing to translators

Lastly, regardless of what we agree upon there probably should be a new tag or filter to get pluralized model names in templates, because AFAIK there is no way to call a model method with an argument directly.

Thanks



Reply all
Reply to author
Forward
0 new messages