Code

Ticket #11688: 11688-verbose_name_plural_evolution-2.diff

File 11688-verbose_name_plural_evolution-2.diff, 85.1 KB (added by ramiro, 2 years ago)

Moved get_verbose_name() method to ._meta Options class, updated and enhanced documentation

Line 
1diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py
2--- a/django/contrib/admin/actions.py
3+++ b/django/contrib/admin/actions.py
4@@ -20,14 +20,15 @@
5 
6     Next, it delets all selected objects and redirects back to the change list.
7     """
8-    opts = modeladmin.model._meta
9+    model = modeladmin.model
10+    opts = model._meta
11     app_label = opts.app_label
12 
13     # Check that the user has delete permission for the actual model
14     if not modeladmin.has_delete_permission(request):
15         raise PermissionDenied
16 
17-    using = router.db_for_write(modeladmin.model)
18+    using = router.db_for_write(model)
19 
20     # Populate deletable_objects, a data structure of all related objects that
21     # will also be deleted.
22@@ -46,15 +47,12 @@
23                 modeladmin.log_deletion(request, obj, obj_display)
24             queryset.delete()
25             modeladmin.message_user(request, _("Successfully deleted %(count)d %(items)s.") % {
26-                "count": n, "items": model_ngettext(modeladmin.opts, n)
27+                "count": n, "items": model_ngettext(model, n)
28             })
29         # Return None to display the change list page again.
30         return None
31 
32-    if len(queryset) == 1:
33-        objects_name = force_unicode(opts.verbose_name)
34-    else:
35-        objects_name = force_unicode(opts.verbose_name_plural)
36+    objects_name = force_unicode(opts.get_verbose_name(len(queryset)))
37 
38     if perms_needed or protected:
39         title = _("Cannot delete %(name)s") % {"name": objects_name}
40diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py
41--- a/django/contrib/admin/filters.py
42+++ b/django/contrib/admin/filters.py
43@@ -150,7 +150,7 @@
44         if hasattr(field, 'verbose_name'):
45             self.lookup_title = field.verbose_name
46         else:
47-            self.lookup_title = other_model._meta.verbose_name
48+            self.lookup_title = other_model._meta.get_verbose_name()
49         rel_name = other_model._meta.pk.name
50         self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name)
51         self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path)
52diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py
53--- a/django/contrib/admin/models.py
54+++ b/django/contrib/admin/models.py
55@@ -2,7 +2,7 @@
56 from django.contrib.contenttypes.models import ContentType
57 from django.contrib.auth.models import User
58 from django.contrib.admin.util import quote
59-from django.utils.translation import ugettext_lazy as _
60+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
61 from django.utils.encoding import smart_unicode
62 from django.utils.safestring import mark_safe
63 
64@@ -27,8 +27,6 @@
65     objects = LogEntryManager()
66 
67     class Meta:
68-        verbose_name = _('log entry')
69-        verbose_name_plural = _('log entries')
70         db_table = 'django_admin_log'
71         ordering = ('-action_time',)
72 
73@@ -66,3 +64,7 @@
74         if self.content_type and self.object_id:
75             return mark_safe(u"%s/%s/%s/" % (self.content_type.app_label, self.content_type.model, quote(self.object_id)))
76         return None
77+
78+    @classmethod
79+    def verbose_names(cls, count=1):
80+        return ungettext_lazy('log entry', 'log entries', count)
81diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
82--- a/django/contrib/admin/options.py
83+++ b/django/contrib/admin/options.py
84@@ -609,7 +609,7 @@
85         """
86         choices = [] + default_choices
87         for func, name, description in self.get_actions(request).itervalues():
88-            choice = (name, description % model_format_dict(self.opts))
89+            choice = (name, description % model_format_dict(self.model))
90             choices.append(choice)
91         return choices
92 
93@@ -674,16 +674,16 @@
94             for formset in formsets:
95                 for added_object in formset.new_objects:
96                     change_message.append(_('Added %(name)s "%(object)s".')
97-                                          % {'name': force_unicode(added_object._meta.verbose_name),
98+                                          % {'name': force_unicode(added_object._meta.get_verbose_name()),
99                                              'object': force_unicode(added_object)})
100                 for changed_object, changed_fields in formset.changed_objects:
101                     change_message.append(_('Changed %(list)s for %(name)s "%(object)s".')
102                                           % {'list': get_text_list(changed_fields, _('and')),
103-                                             'name': force_unicode(changed_object._meta.verbose_name),
104+                                             'name': force_unicode(changed_object._meta.get_verbose_name()),
105                                              'object': force_unicode(changed_object)})
106                 for deleted_object in formset.deleted_objects:
107                     change_message.append(_('Deleted %(name)s "%(object)s".')
108-                                          % {'name': force_unicode(deleted_object._meta.verbose_name),
109+                                          % {'name': force_unicode(deleted_object._meta.get_verbose_name()),
110                                              'object': force_unicode(deleted_object)})
111         change_message = ' '.join(change_message)
112         return change_message or _('No fields changed.')
113@@ -769,7 +769,7 @@
114         opts = obj._meta
115         pk_value = obj._get_pk_val()
116 
117-        msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)}
118+        msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.get_verbose_name()), 'obj': force_unicode(obj)}
119         # Here, we distinguish between different save types by checking for
120         # the presence of keys in request.POST.
121         if "_continue" in request.POST:
122@@ -785,7 +785,7 @@
123                 # escape() calls force_unicode.
124                 (escape(pk_value), escapejs(obj)))
125         elif "_addanother" in request.POST:
126-            self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
127+            self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.get_verbose_name())))
128             return HttpResponseRedirect(request.path)
129         else:
130             self.message_user(request, msg)
131@@ -810,11 +810,11 @@
132 
133         # Handle proxy models automatically created by .only() or .defer().
134         # Refs #14529
135-        verbose_name = opts.verbose_name
136+        verbose_name = opts.get_verbose_name()
137         module_name = opts.module_name
138         if obj._deferred:
139             opts_ = opts.proxy_for_model._meta
140-            verbose_name = opts_.verbose_name
141+            verbose_name = opts_.get_verbose_name()
142             module_name = opts_.module_name
143 
144         pk_value = obj._get_pk_val()
145@@ -995,7 +995,7 @@
146             media = media + inline_admin_formset.media
147 
148         context = {
149-            'title': _('Add %s') % force_unicode(opts.verbose_name),
150+            'title': _('Add %s') % force_unicode(opts.get_verbose_name()),
151             'adminform': adminForm,
152             'is_popup': "_popup" in request.REQUEST,
153             'show_delete': False,
154@@ -1020,7 +1020,7 @@
155             raise PermissionDenied
156 
157         if obj is None:
158-            raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
159+            raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.get_verbose_name()), 'key': escape(object_id)})
160 
161         if request.method == 'POST' and "_saveasnew" in request.POST:
162             return self.add_view(request, form_url=reverse('admin:%s_%s_add' %
163@@ -1086,7 +1086,7 @@
164             media = media + inline_admin_formset.media
165 
166         context = {
167-            'title': _('Change %s') % force_unicode(opts.verbose_name),
168+            'title': _('Change %s') % force_unicode(opts.get_verbose_name()),
169             'adminform': adminForm,
170             'object_id': object_id,
171             'original': obj,
172@@ -1194,15 +1194,11 @@
173                         changecount += 1
174 
175                 if changecount:
176-                    if changecount == 1:
177-                        name = force_unicode(opts.verbose_name)
178-                    else:
179-                        name = force_unicode(opts.verbose_name_plural)
180+                    name = force_unicode(opts.get_verbose_name(changecount))
181                     msg = ungettext("%(count)s %(name)s was changed successfully.",
182                                     "%(count)s %(name)s were changed successfully.",
183                                     changecount) % {'count': changecount,
184-                                                    'name': name,
185-                                                    'obj': force_unicode(obj)}
186+                                                    'name': name}
187                     self.message_user(request, msg)
188 
189                 return HttpResponseRedirect(request.get_full_path())
190@@ -1229,7 +1225,7 @@
191             'All %(total_count)s selected', cl.result_count)
192 
193         context = {
194-            'module_name': force_unicode(opts.verbose_name_plural),
195+            'module_name': force_unicode(opts.get_verbose_name(0)),
196             'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(cl.result_list)},
197             'selection_note_all': selection_note_all % {'total_count': cl.result_count},
198             'title': cl.title,
199@@ -1255,7 +1251,8 @@
200     @transaction.commit_on_success
201     def delete_view(self, request, object_id, extra_context=None):
202         "The 'delete' admin view for this model."
203-        opts = self.model._meta
204+        model = self.model
205+        opts = model._meta
206         app_label = opts.app_label
207 
208         obj = self.get_object(request, unquote(object_id))
209@@ -1264,9 +1261,9 @@
210             raise PermissionDenied
211 
212         if obj is None:
213-            raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
214+            raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.get_verbose_name()), 'key': escape(object_id)})
215 
216-        using = router.db_for_write(self.model)
217+        using = router.db_for_write(model)
218 
219         # Populate deleted_objects, a data structure of all related objects that
220         # will also be deleted.
221@@ -1280,7 +1277,7 @@
222             self.log_deletion(request, obj, obj_display)
223             self.delete_model(request, obj)
224 
225-            self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj_display)})
226+            self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.get_verbose_name()), 'obj': force_unicode(obj_display)})
227 
228             if not self.has_change_permission(request, None):
229                 return HttpResponseRedirect(reverse('admin:index',
230@@ -1289,7 +1286,7 @@
231                                         (opts.app_label, opts.module_name),
232                                         current_app=self.admin_site.name))
233 
234-        object_name = force_unicode(opts.verbose_name)
235+        object_name = force_unicode(opts.get_verbose_name())
236 
237         if perms_needed or protected:
238             title = _("Cannot delete %(name)s") % {"name": object_name}
239@@ -1329,7 +1326,7 @@
240         context = {
241             'title': _('Change history: %s') % force_unicode(obj),
242             'action_list': action_list,
243-            'module_name': capfirst(force_unicode(opts.verbose_name_plural)),
244+            'module_name': capfirst(force_unicode(opts.get_verbose_name(0))),
245             'object': obj,
246             'app_label': app_label,
247             'opts': opts,
248@@ -1365,9 +1362,9 @@
249         self.opts = self.model._meta
250         super(InlineModelAdmin, self).__init__()
251         if self.verbose_name is None:
252-            self.verbose_name = self.model._meta.verbose_name
253+            self.verbose_name = self.model._meta.get_verbose_name()
254         if self.verbose_name_plural is None:
255-            self.verbose_name_plural = self.model._meta.verbose_name_plural
256+            self.verbose_name_plural = self.model._meta.get_verbose_name(0)
257 
258     @property
259     def media(self):
260diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
261--- a/django/contrib/admin/sites.py
262+++ b/django/contrib/admin/sites.py
263@@ -341,7 +341,7 @@
264                 if True in perms.values():
265                     info = (app_label, model._meta.module_name)
266                     model_dict = {
267-                        'name': capfirst(model._meta.verbose_name_plural),
268+                        'name': capfirst(model._meta.get_verbose_name(0)),
269                         'admin_url': reverse('admin:%s_%s_changelist' % info, current_app=self.name),
270                         'add_url': reverse('admin:%s_%s_add' % info, current_app=self.name),
271                         'perms': perms,
272@@ -387,7 +387,7 @@
273                     if True in perms.values():
274                         info = (app_label, model._meta.module_name)
275                         model_dict = {
276-                            'name': capfirst(model._meta.verbose_name_plural),
277+                            'name': capfirst(model._meta.get_verbose_name(0)),
278                             'admin_url': reverse('admin:%s_%s_changelist' % info, current_app=self.name),
279                             'add_url': reverse('admin:%s_%s_add' % info, current_app=self.name),
280                             'perms': perms,
281diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
282--- a/django/contrib/admin/util.py
283+++ b/django/contrib/admin/util.py
284@@ -90,16 +90,16 @@
285             p = '%s.%s' % (opts.app_label,
286                            opts.get_delete_permission())
287             if not user.has_perm(p):
288-                perms_needed.add(opts.verbose_name)
289+                perms_needed.add(opts.get_verbose_name())
290             # Display a link to the admin page.
291             return mark_safe(u'%s: <a href="%s">%s</a>' %
292-                             (escape(capfirst(opts.verbose_name)),
293+                             (escape(capfirst(opts.get_verbose_name())),
294                               admin_url,
295                               escape(obj)))
296         else:
297             # Don't display link to edit, because it either has no
298             # admin or is edited inline.
299-            return u'%s: %s' % (capfirst(opts.verbose_name),
300+            return u'%s: %s' % (capfirst(opts.get_verbose_name()),
301                                 force_unicode(obj))
302 
303     to_delete = collector.nested(format_callback)
304@@ -165,7 +165,8 @@
305     Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
306     typically for use with string formatting.
307 
308-    `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
309+    `obj` may be a `Model` instance, `Model` subclass, a `QuerySet` instance, or
310+    a `django.db.models.Options` instance.
311 
312     """
313     if isinstance(obj, (models.Model, models.base.ModelBase)):
314@@ -175,8 +176,8 @@
315     else:
316         opts = obj
317     return {
318-        'verbose_name': force_unicode(opts.verbose_name),
319-        'verbose_name_plural': force_unicode(opts.verbose_name_plural)
320+        'verbose_name': force_unicode(opts.get_verbose_name()),
321+        'verbose_name_plural': force_unicode(opts.get_verbose_name(0))
322     }
323 
324 
325@@ -194,9 +195,12 @@
326         if n is None:
327             n = obj.count()
328         obj = obj.model
329-    d = model_format_dict(obj)
330-    singular, plural = d["verbose_name"], d["verbose_name_plural"]
331-    return ungettext(singular, plural, n or 0)
332+    else:
333+        if n is None:
334+            n = 0
335+    singular = obj._meta.get_verbose_name(1)
336+    plural = obj._meta.get_verbose_name(n)
337+    return ungettext(singular, plural, n)
338 
339 
340 def lookup_field(name, obj, model_admin=None):
341@@ -229,7 +233,7 @@
342 def label_for_field(name, model, model_admin=None, return_attr=False):
343     """
344     Returns a sensible label for a field name. The name can be a callable or the
345-    name of an object attributes, as well as a genuine fields. If return_attr is
346+    name of an object attribute, as well as a genuine field. If return_attr is
347     True, the resolved attribute (which could be a callable) is also returned.
348     This will be None if (and only if) the name refers to a field.
349     """
350@@ -237,15 +241,15 @@
351     try:
352         field = model._meta.get_field_by_name(name)[0]
353         if isinstance(field, RelatedObject):
354-            label = field.opts.verbose_name
355+            label = field.opts.get_verbose_name()
356         else:
357             label = field.verbose_name
358     except models.FieldDoesNotExist:
359         if name == "__unicode__":
360-            label = force_unicode(model._meta.verbose_name)
361+            label = force_unicode(model._meta.get_verbose_name())
362             attr = unicode
363         elif name == "__str__":
364-            label = smart_str(model._meta.verbose_name)
365+            label = smart_str(model._meta.get_verbose_name())
366             attr = str
367         else:
368             if callable(name):
369@@ -311,7 +315,7 @@
370 
371 
372 def get_model_from_relation(field):
373-    if isinstance(field, models.related.RelatedObject):
374+    if isinstance(field, RelatedObject):
375         return field.model
376     elif getattr(field, 'rel'): # or isinstance?
377         return field.rel.to
378diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
379--- a/django/contrib/admin/views/main.py
380+++ b/django/contrib/admin/views/main.py
381@@ -81,7 +81,7 @@
382             title = ugettext('Select %s')
383         else:
384             title = ugettext('Select %s to change')
385-        self.title = title % force_unicode(self.opts.verbose_name)
386+        self.title = title % force_unicode(self.opts.get_verbose_name())
387         self.pk_attname = self.lookup_opts.pk.attname
388 
389     def get_filters(self, request, use_distinct=False):
390diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
391--- a/django/contrib/auth/models.py
392+++ b/django/contrib/auth/models.py
393@@ -5,8 +5,8 @@
394 from django.db import models
395 from django.db.models.manager import EmptyManager
396 from django.utils.encoding import smart_str
397-from django.utils.translation import ugettext_lazy as _
398 from django.utils import timezone
399+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
400 
401 from django.contrib import auth
402 from django.contrib.auth.signals import user_logged_in
403@@ -54,8 +54,6 @@
404     objects = PermissionManager()
405 
406     class Meta:
407-        verbose_name = _('permission')
408-        verbose_name_plural = _('permissions')
409         unique_together = (('content_type', 'codename'),)
410         ordering = ('content_type__app_label', 'content_type__model', 'codename')
411 
412@@ -69,6 +67,10 @@
413         return (self.codename,) + self.content_type.natural_key()
414     natural_key.dependencies = ['contenttypes.contenttype']
415 
416+    @classmethod
417+    def verbose_names(cls, count=1):
418+        return ungettext_lazy('permission', 'permissions', count)
419+
420 class Group(models.Model):
421     """Groups are a generic way of categorizing users to apply permissions, or some other label, to those users. A user can belong to any number of groups.
422 
423@@ -79,13 +81,13 @@
424     name = models.CharField(_('name'), max_length=80, unique=True)
425     permissions = models.ManyToManyField(Permission, verbose_name=_('permissions'), blank=True)
426 
427-    class Meta:
428-        verbose_name = _('group')
429-        verbose_name_plural = _('groups')
430-
431     def __unicode__(self):
432         return self.name
433 
434+    @classmethod
435+    def verbose_names(cls, count=1):
436+        return ungettext_lazy('group', 'groups', count)
437+
438 class UserManager(models.Manager):
439     def create_user(self, username, email=None, password=None):
440         """
441@@ -188,10 +190,6 @@
442     user_permissions = models.ManyToManyField(Permission, verbose_name=_('user permissions'), blank=True)
443     objects = UserManager()
444 
445-    class Meta:
446-        verbose_name = _('user')
447-        verbose_name_plural = _('users')
448-
449     def __unicode__(self):
450         return self.username
451 
452@@ -337,6 +335,10 @@
453                 raise SiteProfileNotAvailable
454         return self._profile_cache
455 
456+    @classmethod
457+    def verbose_names(cls, count=1):
458+        return ungettext_lazy('user', 'users', count)
459+
460 
461 class AnonymousUser(object):
462     id = None
463diff --git a/django/contrib/comments/models.py b/django/contrib/comments/models.py
464--- a/django/contrib/comments/models.py
465+++ b/django/contrib/comments/models.py
466@@ -5,8 +5,8 @@
467 from django.contrib.sites.models import Site
468 from django.db import models
469 from django.core import urlresolvers
470-from django.utils.translation import ugettext_lazy as _
471 from django.utils import timezone
472+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
473 from django.conf import settings
474 
475 COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000)
476@@ -73,8 +73,6 @@
477         db_table = "django_comments"
478         ordering = ('submit_date',)
479         permissions = [("can_moderate", "Can moderate comments")]
480-        verbose_name = _('comment')
481-        verbose_name_plural = _('comments')
482 
483     def __unicode__(self):
484         return "%s: %s..." % (self.name, self.comment[:50])
485@@ -152,6 +150,10 @@
486         }
487         return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d
488 
489+    @classmethod
490+    def verbose_names(cls, count=1):
491+        return ungettext_lazy('comment', 'comments', count)
492+
493 class CommentFlag(models.Model):
494     """
495     Records a flag on a comment. This is intentionally flexible; right now, a
496@@ -178,8 +180,6 @@
497     class Meta:
498         db_table = 'django_comment_flags'
499         unique_together = [('user', 'comment', 'flag')]
500-        verbose_name = _('comment flag')
501-        verbose_name_plural = _('comment flags')
502 
503     def __unicode__(self):
504         return "%s flag of comment ID %s by %s" % \
505@@ -189,3 +189,7 @@
506         if self.flag_date is None:
507             self.flag_date = timezone.now()
508         super(CommentFlag, self).save(*args, **kwargs)
509+
510+    @classmethod
511+    def verbose_names(cls, count=1):
512+        return ungettext_lazy('comment flag', 'comment flags', count)
513diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py
514--- a/django/contrib/contenttypes/models.py
515+++ b/django/contrib/contenttypes/models.py
516@@ -1,5 +1,5 @@
517 from django.db import models
518-from django.utils.translation import ugettext_lazy as _
519+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
520 from django.utils.encoding import smart_unicode, force_unicode
521 
522 class ContentTypeManager(models.Manager):
523@@ -127,8 +127,6 @@
524     objects = ContentTypeManager()
525 
526     class Meta:
527-        verbose_name = _('content type')
528-        verbose_name_plural = _('content types')
529         db_table = 'django_content_type'
530         ordering = ('name',)
531         unique_together = (('app_label', 'model'),)
532@@ -145,7 +143,7 @@
533         if not model or self.name != model._meta.verbose_name_raw:
534             return self.name
535         else:
536-            return force_unicode(model._meta.verbose_name)
537+            return force_unicode(model._meta.get_verbose_name())
538 
539     def model_class(self):
540         "Returns the Python model class for this type of content."
541@@ -170,3 +168,7 @@
542 
543     def natural_key(self):
544         return (self.app_label, self.model)
545+
546+    @classmethod
547+    def verbose_names(cls, count=1):
548+        return ungettext_lazy('content type', 'content types', count)
549diff --git a/django/contrib/databrowse/datastructures.py b/django/contrib/databrowse/datastructures.py
550--- a/django/contrib/databrowse/datastructures.py
551+++ b/django/contrib/databrowse/datastructures.py
552@@ -18,8 +18,8 @@
553         self.site = site
554         self.model = model
555         self.model_list = site.registry.keys()
556-        self.verbose_name = model._meta.verbose_name
557-        self.verbose_name_plural = model._meta.verbose_name_plural
558+        self.verbose_name = model._meta.get_verbose_name()
559+        self.verbose_name_plural = model._meta.get_verbose_name(0)
560 
561     def __repr__(self):
562         return '<EasyModel for %s>' % smart_str(self.model._meta.object_name)
563diff --git a/django/contrib/flatpages/models.py b/django/contrib/flatpages/models.py
564--- a/django/contrib/flatpages/models.py
565+++ b/django/contrib/flatpages/models.py
566@@ -1,6 +1,6 @@
567 from django.db import models
568 from django.contrib.sites.models import Site
569-from django.utils.translation import ugettext_lazy as _
570+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
571 
572 
573 class FlatPage(models.Model):
574@@ -15,8 +15,6 @@
575 
576     class Meta:
577         db_table = 'django_flatpage'
578-        verbose_name = _('flat page')
579-        verbose_name_plural = _('flat pages')
580         ordering = ('url',)
581 
582     def __unicode__(self):
583@@ -24,3 +22,7 @@
584 
585     def get_absolute_url(self):
586         return self.url
587+
588+    @classmethod
589+    def verbose_names(cls, count=1):
590+        return ungettext_lazy('flat page', 'flat pages', count)
591diff --git a/django/contrib/redirects/models.py b/django/contrib/redirects/models.py
592--- a/django/contrib/redirects/models.py
593+++ b/django/contrib/redirects/models.py
594@@ -1,6 +1,6 @@
595 from django.db import models
596 from django.contrib.sites.models import Site
597-from django.utils.translation import ugettext_lazy as _
598+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
599 
600 class Redirect(models.Model):
601     site = models.ForeignKey(Site)
602@@ -10,11 +10,13 @@
603         help_text=_("This can be either an absolute path (as above) or a full URL starting with 'http://'."))
604 
605     class Meta:
606-        verbose_name = _('redirect')
607-        verbose_name_plural = _('redirects')
608         db_table = 'django_redirect'
609         unique_together=(('site', 'old_path'),)
610         ordering = ('old_path',)
611-   
612+
613     def __unicode__(self):
614         return "%s ---> %s" % (self.old_path, self.new_path)
615+
616+    @classmethod
617+    def verbose_names(cls, count=1):
618+        return ungettext_lazy('redirect', 'redirects', count)
619diff --git a/django/contrib/sessions/models.py b/django/contrib/sessions/models.py
620--- a/django/contrib/sessions/models.py
621+++ b/django/contrib/sessions/models.py
622@@ -1,5 +1,5 @@
623 from django.db import models
624-from django.utils.translation import ugettext_lazy as _
625+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
626 
627 
628 class SessionManager(models.Manager):
629@@ -43,12 +43,14 @@
630 
631     class Meta:
632         db_table = 'django_session'
633-        verbose_name = _('session')
634-        verbose_name_plural = _('sessions')
635 
636     def get_decoded(self):
637         return SessionStore().decode(self.session_data)
638 
639+    @classmethod
640+    def verbose_names(cls, count=1):
641+        return ungettext_lazy('session', 'sessions', count)
642+
643 
644 # At bottom to avoid circular import
645 from django.contrib.sessions.backends.db import SessionStore
646diff --git a/django/contrib/sites/models.py b/django/contrib/sites/models.py
647--- a/django/contrib/sites/models.py
648+++ b/django/contrib/sites/models.py
649@@ -1,5 +1,5 @@
650 from django.db import models
651-from django.utils.translation import ugettext_lazy as _
652+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
653 
654 
655 SITE_CACHE = {}
656@@ -40,8 +40,6 @@
657 
658     class Meta:
659         db_table = 'django_site'
660-        verbose_name = _('site')
661-        verbose_name_plural = _('sites')
662         ordering = ('domain',)
663 
664     def __unicode__(self):
665@@ -61,6 +59,10 @@
666         except KeyError:
667             pass
668 
669+    @classmethod
670+    def verbose_names(cls, count=1):
671+        return ungettext_lazy('site', 'sites', count)
672+
673 
674 class RequestSite(object):
675     """
676diff --git a/django/db/models/base.py b/django/db/models/base.py
677--- a/django/db/models/base.py
678+++ b/django/db/models/base.py
679@@ -98,6 +98,10 @@
680         for obj_name, obj in attrs.items():
681             new_class.add_to_class(obj_name, obj)
682 
683+        if hasattr(new_class, 'verbose_names'):
684+            new_class._meta._verbose_name = new_class._meta.get_verbose_name()
685+            new_class._meta._verbose_name_plural = new_class._meta.get_verbose_name(0)
686+
687         # All the fields of any type declared on this model
688         new_fields = new_class._meta.local_fields + \
689                      new_class._meta.local_many_to_many + \
690@@ -770,7 +774,7 @@
691 
692     def unique_error_message(self, model_class, unique_check):
693         opts = model_class._meta
694-        model_name = capfirst(opts.verbose_name)
695+        model_name = capfirst(opts.get_verbose_name())
696 
697         # A unique field
698         if len(unique_check) == 1:
699diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
700--- a/django/db/models/fields/related.py
701+++ b/django/db/models/fields/related.py
702@@ -115,7 +115,7 @@
703     def set_attributes_from_rel(self):
704         self.name = self.name or (self.rel.to._meta.object_name.lower() + '_' + self.rel.to._meta.pk.name)
705         if self.verbose_name is None:
706-            self.verbose_name = self.rel.to._meta.verbose_name
707+            self.verbose_name = self.rel.to._meta.get_verbose_name()
708         self.rel.field_name = self.rel.field_name or self.rel.to._meta.pk.name
709 
710     def do_related_class(self, other, cls):
711@@ -940,7 +940,7 @@
712         qs = qs.complex_filter(self.rel.limit_choices_to)
713         if not qs.exists():
714             raise exceptions.ValidationError(self.error_messages['invalid'] % {
715-                'model': self.rel.to._meta.verbose_name, 'pk': value})
716+                'model': self.rel.to._meta.get_verbose_name(), 'pk': value})
717 
718     def get_attname(self):
719         return '%s_id' % self.name
720@@ -1052,6 +1052,12 @@
721 def create_many_to_many_intermediary_model(field, klass):
722     from django.db import models
723     managed = True
724+
725+    def verbose_name_method(f, t):
726+        def verbose_names(cls, count=1):
727+            return '%(from)s-%(to)s relationship%(extra)s' % {'from': f, 'to': t, 'extra': 's' if count != 1 else ''}
728+        return verbose_names
729+
730     if isinstance(field.rel.to, basestring) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT:
731         to_model = field.rel.to
732         to = to_model.split('.')[-1]
733@@ -1080,15 +1086,14 @@
734         'app_label': klass._meta.app_label,
735         'db_tablespace': klass._meta.db_tablespace,
736         'unique_together': (from_, to),
737-        'verbose_name': '%(from)s-%(to)s relationship' % {'from': from_, 'to': to},
738-        'verbose_name_plural': '%(from)s-%(to)s relationships' % {'from': from_, 'to': to},
739     })
740     # Construct and return the new class.
741     return type(name, (models.Model,), {
742         'Meta': meta,
743         '__module__': klass.__module__,
744         from_: models.ForeignKey(klass, related_name='%s+' % name, db_tablespace=field.db_tablespace),
745-        to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace)
746+        to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace),
747+        'verbose_names': classmethod(verbose_name_method(from_, to)),
748     })
749 
750 class ManyToManyField(RelatedField, Field):
751diff --git a/django/db/models/options.py b/django/db/models/options.py
752--- a/django/db/models/options.py
753+++ b/django/db/models/options.py
754@@ -1,30 +1,34 @@
755+from bisect import bisect
756 import re
757-from bisect import bisect
758+import warnings
759 
760 from django.conf import settings
761-from django.db.models.related import RelatedObject
762 from django.db.models.fields.related import ManyToManyRel
763 from django.db.models.fields import AutoField, FieldDoesNotExist
764 from django.db.models.fields.proxy import OrderWrt
765 from django.db.models.loading import get_models, app_cache_ready
766+from django.db.models.related import RelatedObject
767+from django.utils.datastructures import SortedDict
768+from django.utils.encoding import force_unicode, smart_str
769+from django.utils.functional import cached_property
770 from django.utils.translation import activate, deactivate_all, get_language, string_concat
771-from django.utils.encoding import force_unicode, smart_str
772-from django.utils.datastructures import SortedDict
773 
774-# Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces".
775-get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip()
776-
777-DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
778+DEFAULT_NAMES = ('db_table', 'ordering',
779                  'unique_together', 'permissions', 'get_latest_by',
780                  'order_with_respect_to', 'app_label', 'db_tablespace',
781                  'abstract', 'managed', 'proxy', 'auto_created')
782 
783+DEPRECATED_NAMES = ('verbose_name', 'verbose_name_plural')
784+
785+CAMEL_CASE_RE = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
786+
787 class Options(object):
788     def __init__(self, meta, app_label=None):
789         self.local_fields, self.local_many_to_many = [], []
790         self.virtual_fields = []
791-        self.module_name, self.verbose_name = None, None
792-        self.verbose_name_plural = None
793+        self.cls = None
794+        self.module_name, self._verbose_name = None, None
795+        self._verbose_name_plural = None
796         self.db_table = ''
797         self.ordering = []
798         self.unique_together =  []
799@@ -54,16 +58,34 @@
800         # from *other* models. Needed for some admin checks. Internal use only.
801         self.related_fkey_lookups = []
802 
803+    def _get_verbose_name(self):
804+        warnings.warn("Meta.verbose_name is deprecated. Use a verbose_names()"
805+            " classmethod in the model instead.", PendingDeprecationWarning)
806+        return self._verbose_name
807+    def _set_verbose_name(self, value):
808+        self._verbose_name = value
809+    verbose_name = property(_get_verbose_name, _set_verbose_name)
810+
811+    def _get_verbose_name_plural(self):
812+        warnings.warn("Meta.verbose_name_plural is deprecated. Use a "
813+            "verbose_names() classmethod in the model instead.",
814+            PendingDeprecationWarning)
815+        return self._verbose_name_plural
816+    def _set_verbose_name_plural(self, value):
817+        self._verbose_name_plural = value
818+    verbose_name_plural = property(_get_verbose_name_plural, _set_verbose_name_plural)
819+
820     def contribute_to_class(self, cls, name):
821         from django.db import connection
822         from django.db.backends.util import truncate_name
823 
824+        self.cls = cls
825         cls._meta = self
826         self.installed = re.sub('\.models$', '', cls.__module__) in settings.INSTALLED_APPS
827         # First, construct the default values for these options.
828         self.object_name = cls.__name__
829         self.module_name = self.object_name.lower()
830-        self.verbose_name = get_verbose_name(self.object_name)
831+        self._verbose_name = CAMEL_CASE_RE.sub(' \\1', self.object_name).lower().strip()
832 
833         # Next, apply any overridden values from 'class Meta'.
834         if self.meta:
835@@ -80,6 +102,14 @@
836                 elif hasattr(self.meta, attr_name):
837                     setattr(self, attr_name, getattr(self.meta, attr_name))
838 
839+            for attr_name in DEPRECATED_NAMES:
840+                if attr_name in meta_attrs:
841+                    warnings.warn("%(cls)s: Meta.%(attr_name)s is deprecated. Use a "
842+                        "verbose_names() classmethod in the model "
843+                        "instead." % {'cls': cls, 'attr_name': attr_name},
844+                        PendingDeprecationWarning)
845+                    setattr(self, '_%s' % attr_name, meta_attrs.pop(attr_name))
846+
847             # unique_together can be either a tuple of tuples, or a single
848             # tuple of two strings. Normalize it to a tuple of tuples, so that
849             # calling code can uniformly expect that.
850@@ -90,14 +120,14 @@
851 
852             # verbose_name_plural is a special case because it uses a 's'
853             # by default.
854-            if self.verbose_name_plural is None:
855-                self.verbose_name_plural = string_concat(self.verbose_name, 's')
856+            if self._verbose_name_plural is None:
857+                self._verbose_name_plural = string_concat(self._verbose_name, 's')
858 
859             # Any leftover attributes must be invalid.
860             if meta_attrs != {}:
861                 raise TypeError("'class Meta' got invalid attribute(s): %s" % ','.join(meta_attrs.keys()))
862         else:
863-            self.verbose_name_plural = string_concat(self.verbose_name, 's')
864+            self._verbose_name_plural = string_concat(self._verbose_name, 's')
865         del self.meta
866 
867         # If the db_table wasn't provided, use the app_label + module_name.
868@@ -199,10 +229,10 @@
869         """
870         lang = get_language()
871         deactivate_all()
872-        raw = force_unicode(self.verbose_name)
873+        raw = force_unicode(self._verbose_name)
874         activate(lang)
875         return raw
876-    verbose_name_raw = property(verbose_name_raw)
877+    verbose_name_raw = cached_property(verbose_name_raw)
878 
879     def _fields(self):
880         """
881@@ -495,3 +525,12 @@
882         Returns the index of the primary key field in the self.fields list.
883         """
884         return self.fields.index(self.pk)
885+
886+    def get_verbose_name(self, count=1):
887+        if hasattr(self.cls, 'verbose_names'):
888+            retv = self.cls.verbose_names(count)
889+            if retv is not None:
890+                return retv
891+        if count == 1:
892+            return CAMEL_CASE_RE.sub(' \\1', self.object_name).lower().strip()
893+        return string_concat(self.get_verbose_name(1), 's')
894diff --git a/django/db/models/related.py b/django/db/models/related.py
895--- a/django/db/models/related.py
896+++ b/django/db/models/related.py
897@@ -36,7 +36,7 @@
898                 {'%s__isnull' % self.parent_model._meta.module_name: False})
899         lst = [(x._get_pk_val(), smart_unicode(x)) for x in queryset]
900         return first_choice + lst
901-       
902+
903     def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
904         # Defer to the actual field definition for db prep
905         return self.field.get_db_prep_lookup(lookup_type, value,
906@@ -67,3 +67,6 @@
907 
908     def get_cache_name(self):
909         return "_%s_cache" % self.get_accessor_name()
910+
911+    def get_verbose_name(self, count=1):
912+        return self.model._meta.get_verbose_name(count)
913diff --git a/django/utils/functional.py b/django/utils/functional.py
914--- a/django/utils/functional.py
915+++ b/django/utils/functional.py
916@@ -31,8 +31,8 @@
917 
918 class cached_property(object):
919     """
920-    Decorator that creates converts a method with a single
921-    self argument into a property cached on the instance.
922+    Decorator that converts a method with a single self argument into a
923+    property cached on the instance.
924     """
925     def __init__(self, func):
926         self.func = func
927diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py
928--- a/django/views/generic/create_update.py
929+++ b/django/views/generic/create_update.py
930@@ -95,7 +95,7 @@
931         return model.objects.get(**lookup_kwargs)
932     except ObjectDoesNotExist:
933         raise Http404("No %s found for %s"
934-                      % (model._meta.verbose_name, lookup_kwargs))
935+                      % (model._meta.get_verbose_name(), lookup_kwargs))
936 
937 def create_object(request, model=None, template_name=None,
938         template_loader=loader, extra_context=None, post_save_redirect=None,
939@@ -119,7 +119,7 @@
940             new_object = form.save()
941 
942             msg = ugettext("The %(verbose_name)s was created successfully.") %\
943-                                    {"verbose_name": model._meta.verbose_name}
944+                                    {"verbose_name": model._meta.get_verbose_name()}
945             messages.success(request, msg, fail_silently=True)
946             return redirect(post_save_redirect, new_object)
947     else:
948@@ -162,7 +162,7 @@
949         if form.is_valid():
950             obj = form.save()
951             msg = ugettext("The %(verbose_name)s was updated successfully.") %\
952-                                    {"verbose_name": model._meta.verbose_name}
953+                                    {"verbose_name": model._meta.get_verbose_name()}
954             messages.success(request, msg, fail_silently=True)
955             return redirect(post_save_redirect, obj)
956     else:
957@@ -205,7 +205,7 @@
958     if request.method == 'POST':
959         obj.delete()
960         msg = ugettext("The %(verbose_name)s was deleted.") %\
961-                                    {"verbose_name": model._meta.verbose_name}
962+                                    {"verbose_name": model._meta.get_verbose_name()}
963         messages.success(request, msg, fail_silently=True)
964         return HttpResponseRedirect(post_delete_redirect)
965     else:
966diff --git a/django/views/generic/date_based.py b/django/views/generic/date_based.py
967--- a/django/views/generic/date_based.py
968+++ b/django/views/generic/date_based.py
969@@ -34,7 +34,7 @@
970         queryset = queryset.filter(**{'%s__lte' % date_field: datetime.datetime.now()})
971     date_list = queryset.dates(date_field, 'year')[::-1]
972     if not date_list and not allow_empty:
973-        raise Http404("No %s available" % model._meta.verbose_name)
974+        raise Http404("No %s available" % model._meta.get_verbose_name())
975 
976     if date_list and num_latest:
977         latest = queryset.order_by('-'+date_field)[:num_latest]
978@@ -354,7 +354,7 @@
979     try:
980         obj = queryset.get(**lookup_kwargs)
981     except ObjectDoesNotExist:
982-        raise Http404("No %s found for" % model._meta.verbose_name)
983+        raise Http404("No %s found for" % model._meta.get_verbose_name())
984     if not template_name:
985         template_name = "%s/%s_detail.html" % (model._meta.app_label, model._meta.object_name.lower())
986     if template_name_field:
987diff --git a/django/views/generic/dates.py b/django/views/generic/dates.py
988--- a/django/views/generic/dates.py
989+++ b/django/views/generic/dates.py
990@@ -195,7 +195,7 @@
991 
992         if not allow_empty and not qs:
993             raise Http404(_(u"No %(verbose_name_plural)s available") % {
994-                    'verbose_name_plural': force_unicode(qs.model._meta.verbose_name_plural)
995+                    'verbose_name_plural': force_unicode(qs.model._meta.get_verbose_name(0))
996             })
997 
998         return qs
999@@ -464,7 +464,7 @@
1000 
1001         if not self.get_allow_future() and date > datetime.date.today():
1002             raise Http404(_(u"Future %(verbose_name_plural)s not available because %(class_name)s.allow_future is False.") % {
1003-                'verbose_name_plural': qs.model._meta.verbose_name_plural,
1004+                'verbose_name_plural': qs.model._meta.get_verbose_name(0),
1005                 'class_name': self.__class__.__name__,
1006             })
1007 
1008diff --git a/django/views/generic/detail.py b/django/views/generic/detail.py
1009--- a/django/views/generic/detail.py
1010+++ b/django/views/generic/detail.py
1011@@ -49,7 +49,7 @@
1012             obj = queryset.get()
1013         except ObjectDoesNotExist:
1014             raise Http404(_(u"No %(verbose_name)s found matching the query") %
1015-                          {'verbose_name': queryset.model._meta.verbose_name})
1016+                          {'verbose_name': queryset.model._meta.get_verbose_name()})
1017         return obj
1018 
1019     def get_queryset(self):
1020diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py
1021--- a/django/views/generic/list_detail.py
1022+++ b/django/views/generic/list_detail.py
1023@@ -131,7 +131,7 @@
1024     try:
1025         obj = queryset.get()
1026     except ObjectDoesNotExist:
1027-        raise Http404("No %s found matching the query" % (model._meta.verbose_name))
1028+        raise Http404("No %s found matching the query" % (model._meta.get_verbose_name()))
1029     if not template_name:
1030         template_name = "%s/%s_detail.html" % (model._meta.app_label, model._meta.object_name.lower())
1031     if template_name_field:
1032diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt
1033--- a/docs/ref/models/instances.txt
1034+++ b/docs/ref/models/instances.txt
1035@@ -540,12 +540,106 @@
1036 More details on named URL patterns are in the :doc:`URL dispatch documentation
1037 </topics/http/urls>`.
1038 
1039+``verbose_names``
1040+-----------------
1041+
1042+.. classmethod:: Model.verbose_names(count=1)
1043+
1044+.. versionadded:: 1.4
1045+
1046+A Python classmethod.
1047+
1048+This method has no default implementation and you might provide one depending
1049+on the the human readable name you need for your model.
1050+
1051+The *count* argument is the quantity of model instances the verbose name is
1052+being requested for.
1053+
1054+It provides a straight migration path from the :attr:`~Options.verbose_name` and
1055+:attr:`~Options.verbose_name_plural` options that entered a deprecation cycle
1056+starting with Django 1.4.  For example this model declaration::
1057+
1058+    class SSN(models.Model):
1059+        value = models.CharField(max_length=11)
1060+
1061+        class Meta:
1062+            verbose_name = 'social security number'
1063+
1064+needs to be changed to::
1065+
1066+    class SSN(models.Model):
1067+        value = models.CharField(max_length=11)
1068+
1069+        @classmethod
1070+        def verbose_names(cls, count=1):
1071+            return 'social security number'
1072+
1073+and this one::
1074+
1075+    class SecurityPolicy(models.Model):
1076+        title = models.CharField(max_length=30)
1077+
1078+        class Meta:
1079+            verbose_name_plural = 'security policies'
1080+
1081+should be changed to::
1082+
1083+    class SecurityPolicy(models.Model):
1084+        title = models.CharField(max_length=30)
1085+
1086+        @classmethod
1087+        def verbose_names(cls, count=1):
1088+            if count != 1:
1089+                return 'security policies'
1090+
1091+This new syntax can take in account the number of model instances at play to
1092+decide the exact verbose name to show in user interaction contexts. It provides
1093+for better internationalization of your application because the name of your
1094+model is now translatable in a more correct way to many more locales::
1095+
1096+    from django.utils.translation import ugettext_lazy
1097+
1098+    class Man(models.Model):
1099+        first_name = models.CharField(max_length=30)
1100+
1101+        @classmethod
1102+        def verbose_names(cls, count=1):
1103+            if count == 1:
1104+                return ugettext_lazy('man')
1105+            else:
1106+                return ugettext_lazy('men')
1107+
1108+Although you usually will use the
1109+:func:`~django.utils.translation.ungettext_lazy` function::
1110+
1111+    from django.utils.translation import ungettext_lazy
1112+
1113+    class Library(models.Model):
1114+        city_name = models.CharField(max_length=30)
1115+
1116+        @classmethod
1117+        def verbose_names(cls, count=1):
1118+            return ungetttext_lazy('llbrary', 'libraries', count)
1119+
1120+.. note::
1121+    Remember to declare this method as a classmethod::
1122+
1123+        class MyModel(models.Model):
1124+            ...
1125+
1126+            @classmethod
1127+            def verbose_names(cls, count=1):
1128+                ...
1129+
1130 Extra instance methods
1131 ======================
1132 
1133 In addition to :meth:`~Model.save()`, :meth:`~Model.delete()`, a model object
1134 might have some of the following methods:
1135 
1136+``get_*_display``
1137+-----------------
1138+
1139 .. method:: Model.get_FOO_display()
1140 
1141 For every field that has :attr:`~django.db.models.Field.choices` set, the
1142@@ -570,6 +664,9 @@
1143     >>> p.get_gender_display()
1144     'Male'
1145 
1146+``get_next_by_*`` and ``get_prev_by_*``
1147+---------------------------------------
1148+
1149 .. method:: Model.get_next_by_FOO(\**kwargs)
1150 .. method:: Model.get_previous_by_FOO(\**kwargs)
1151 
1152@@ -586,4 +683,3 @@
1153 Note that in the case of identical date values, these methods will use the
1154 primary key as a tie-breaker. This guarantees that no records are skipped or
1155 duplicated. That also means you cannot use those methods on unsaved objects.
1156-
1157diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt
1158--- a/docs/ref/models/options.txt
1159+++ b/docs/ref/models/options.txt
1160@@ -253,6 +253,11 @@
1161 
1162 .. attribute:: Options.verbose_name
1163 
1164+    .. deprecated:: 1.4
1165+        This option has been replaced by the :meth:`~Model.verbose_names` model
1166+        classmethod. Implement such method in your model to make Django aware
1167+        of its human readable name(s).
1168+
1169     A human-readable name for the object, singular::
1170 
1171         verbose_name = "pizza"
1172@@ -265,8 +270,72 @@
1173 
1174 .. attribute:: Options.verbose_name_plural
1175 
1176+    .. deprecated:: 1.4
1177+        This option has been replaced by the :meth:`~Model.verbose_names` model
1178+        classmethod. Implement such method in your model to make Django aware
1179+        of its human readable name(s).
1180+
1181     The plural name for the object::
1182 
1183         verbose_name_plural = "stories"
1184 
1185     If this isn't given, Django will use :attr:`~Options.verbose_name` + ``"s"``.
1186+
1187+``Meta`` methods
1188+================
1189+
1190+``get_verbose_name``
1191+--------------------
1192+
1193+.. method:: Options.get_verbose_name(count=1)
1194+
1195+.. versionadded:: 1.4
1196+
1197+It provides an API to access translated and correctly pluralized verbose names
1198+of models (something that previously involved accessing the
1199+``Model._meta.verbose_name`` and ``Model._meta.verbose_name_plural``
1200+attributes.)
1201+
1202+.. seealso::
1203+
1204+    The :meth:`~Model.verbose_names` user-provided classmethod that works
1205+    together with this method.
1206+
1207+This method will always return a value independently of whether the model
1208+implements the :meth:`~Model.verbose_names` classmethod or not. Django provides
1209+fallback return values compatible with the default values of the deprecated
1210+:attr:`~Options.verbose_name` and :attr:`~Options.verbose_name_plural` options.
1211+
1212+For example, given this model::
1213+
1214+    class Door(models.Model):
1215+        height = models.PositiveIntegerField()
1216+
1217+then these are the return values of this method::
1218+
1219+    >>> Door._meta.get_verbose_name(1) # One door
1220+    >>> 'door' # Automatically provided singular verbose name
1221+    >>> Door._meta.get_verbose_name(3) # More than one door
1222+    >>> 'doors'
1223+    # Note how it returns an automatically provided simple naive pluralization
1224+    # appending a 's' to the singular value
1225+
1226+Or, for the examples in the :meth:`~Model.verbose_names` documentation::
1227+
1228+    >>> SSN._meta.get_verbose_name() # One SSN, count default value
1229+    >>> 'social security number'
1230+    # Note how it returns the value returned by SSN.verbose_names(count) for a
1231+    # value of count=1
1232+    >>> SSN._meta.get_verbose_name(0) # Zero SSN
1233+    >>> 'social security numbers'
1234+    # Note how it returns an automatically provided simple naive pluralization
1235+    # appending a 's' to the singular value
1236+
1237+    >>> SecurityPolicy._meta.get_verbose_name() # One policy
1238+    >>> 'security policy'
1239+    # Note how it returns a value automatically provided by Django by processing
1240+    # the model class name
1241+    >>> SecurityPolicy._meta.get_verbose_name(10) # Ten policies
1242+    >>> 'security policies'
1243+    # Note how it returns the value returned by
1244+    # SecurityPolicy.verbose_names(count) for a count value different from 1
1245diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt
1246--- a/docs/topics/db/models.txt
1247+++ b/docs/topics/db/models.txt
1248@@ -641,13 +641,10 @@
1249 
1250         class Meta:
1251             ordering = ["horn_length"]
1252-            verbose_name_plural = "oxen"
1253 
1254 Model metadata is "anything that's not a field", such as ordering options
1255-(:attr:`~Options.ordering`), database table name (:attr:`~Options.db_table`), or
1256-human-readable singular and plural names (:attr:`~Options.verbose_name` and
1257-:attr:`~Options.verbose_name_plural`). None are required, and adding ``class
1258-Meta`` to a model is completely optional.
1259+(:attr:`~Options.ordering`), database table name (:attr:`~Options.db_table`).
1260+None are required, and adding ``class Meta`` to a model is completely optional.
1261 
1262 A complete list of all possible ``Meta`` options can be found in the :doc:`model
1263 option reference </ref/models/options>`.
1264diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt
1265--- a/docs/topics/i18n/translation.txt
1266+++ b/docs/topics/i18n/translation.txt
1267@@ -168,11 +168,10 @@
1268 translation string and the number of objects.
1269 
1270 This function is useful when you need your Django application to be localizable
1271-to languages where the number and complexity of `plural forms
1272-<http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms>`_ is
1273-greater than the two forms used in English ('object' for the singular and
1274-'objects' for all the cases where ``count`` is different from one, irrespective
1275-of its value.)
1276+to languages where the number and complexity of `plural forms`_ is greater than
1277+the two forms as used in the English language (e.g. 'object' for the singular
1278+and 'objects' for all the cases where ``count`` is different from one,
1279+irrespective of its value.)
1280 
1281 For example::
1282 
1283@@ -187,18 +186,15 @@
1284         }
1285         return HttpResponse(page)
1286 
1287-In this example the number of objects is passed to the translation
1288-languages as the ``count`` variable.
1289+In this example the number of objects is passed to the translation functions as
1290+the ``count`` variable.
1291 
1292-Lets see a slightly more complex usage example::
1293+Lets see a slightly more complex example::
1294 
1295     from django.utils.translation import ungettext
1296 
1297     count = Report.objects.count()
1298-    if count == 1:
1299-        name = Report._meta.verbose_name
1300-    else:
1301-        name = Report._meta.verbose_name_plural
1302+    name = Report._meta.get_verbose_name(count)
1303 
1304     text = ungettext(
1305             'There is %(count)d %(name)s available.',
1306@@ -209,10 +205,12 @@
1307         'name': name
1308     }
1309 
1310-Here we reuse localizable, hopefully already translated literals (contained in
1311-the ``verbose_name`` and ``verbose_name_plural`` model ``Meta`` options) for
1312-other parts of the sentence so all of it is consistently based on the
1313-cardinality of the elements at play.
1314+Here we reuse localizable, potentially translated literals (as returned by the
1315+:meth:`django.db.models.Options.get_verbose_name` method) for other parts of the
1316+sentence so all of it is consistently based on the cardinality of the elements
1317+at play.
1318+
1319+.. _plural forms: http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms
1320 
1321 .. _pluralization-var-notes:
1322 
1323@@ -294,11 +292,24 @@
1324 Lazy translation
1325 ----------------
1326 
1327-Use the function :func:`django.utils.translation.ugettext_lazy()` to translate
1328-strings lazily -- when the value is accessed rather than when the
1329-``ugettext_lazy()`` function is called.
1330+Use the lazy versions of translation functions in
1331+:mod:`django.utils.translation` (easily recognizable by the ``lazy`` suffix in
1332+their names) to translate strings lazily -- when the value is accessed rather
1333+than when they are called.
1334 
1335-For example, to translate a model's ``help_text``, do the following::
1336+This is an essential need when we need to mark for translation text that is
1337+intermixed with code that is executed at module load time.
1338+
1339+As this is something that can easily happen when defining Django models (the
1340+declarative notation is implemented in a way such that model fields are actually
1341+class level attributes) this means you need to make sure to use lazy
1342+translations in the following cases:
1343+
1344+Model fields and relationship ``verbose_name`` and ``help_text`` option values
1345+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1346+
1347+For example, to translate the help text of the *name* field in the following
1348+model, do the following::
1349 
1350     from django.utils.translation import ugettext_lazy
1351 
1352@@ -310,6 +321,65 @@
1353 is used in a string context, such as template rendering on the Django admin
1354 site.
1355 
1356+In addition to normal fields your models might also contain relationships:
1357+``ForeignKey``, ``ManyTomanyField`` or ``OneToOneField`` fields. You can mark
1358+the name of a relationship itself as translatable by using its ``verbose_name``
1359+option::
1360+
1361+    from django.utils.translation import ugettext_lazy as _
1362+
1363+    class MyThing(models.Model):
1364+        kind = models.ForeignKey(ThingKind, related_name='kinds',
1365+                                 verbose_name=_('kind'))
1366+
1367+Just like you would do in the :meth:`~django.db.models.Model.verbose_names`
1368+model classmethod, you should provide a lowercase verbose name text for the
1369+relation as Django will automatically titlecase it when required.
1370+
1371+Model verbose names values
1372+~~~~~~~~~~~~~~~~~~~~~~~~~~
1373+
1374+It is recommended to always provide a
1375+:meth:`~django.db.models.Model.verbose_names` method rather than relying on
1376+Django's default English-centric and somewhat naïve determination of verbose
1377+names::
1378+
1379+    from django.utils.translation import ugettext_lazy
1380+
1381+    class MyThing(models.Model):
1382+        name = models.CharField(_('name'), help_text=ugettext_lazy('This is the help text'))
1383+
1384+        @classmethod
1385+        def verbose_names(cls, count=1):
1386+            if count == 1:
1387+                return ugettext_lazy('my thing')
1388+            else:
1389+                return ugettext_lazy('my things')
1390+
1391+In this particular case it is almost always a better idea to actually use
1392+:func:`~django.utils.translation.ungettext_lazy` instead of ``ugettext_lazy``.
1393+Refer to the ``verbose_names()`` documentation linked above for details on how
1394+this can make the lifes of your translators and users easier.
1395+
1396+Model methods ``short_description`` attribute values
1397+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1398+
1399+For model methods, you can provide translations to Django and the admin site
1400+with the ``short_description`` parameter set on the corresponding method::
1401+
1402+    from django.utils.translation import ugettext_lazy as _
1403+
1404+    class MyThing(models.Model):
1405+        kind = models.ForeignKey(ThingKind, related_name='kinds',
1406+                                 verbose_name=_('kind'))
1407+
1408+        def is_mouse(self):
1409+            return self.kind.type == MOUSE_TYPE
1410+        is_mouse.short_description = _('Is it a mouse?')
1411+
1412+Notes on translation in models
1413+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1414+
1415 The result of a ``ugettext_lazy()`` call can be used wherever you would use a
1416 unicode string (an object with type ``unicode``) in Python. If you try to use
1417 it where a bytestring (a ``str`` object) is expected, things will not work as
1418@@ -328,7 +398,7 @@
1419 <django.utils.functional...>"``, you have tried to insert the result of
1420 ``ugettext_lazy()`` into a bytestring. That's a bug in your code.
1421 
1422-If you don't like the verbose name ``ugettext_lazy``, you can just alias it as
1423+If you don't like the long ``ugettext_lazy`` name, you can just alias it as
1424 ``_`` (underscore), like so::
1425 
1426     from django.utils.translation import ugettext_lazy as _
1427@@ -336,56 +406,6 @@
1428     class MyThing(models.Model):
1429         name = models.CharField(help_text=_('This is the help text'))
1430 
1431-Always use lazy translations in :doc:`Django models </topics/db/models>`.
1432-Field names and table names should be marked for translation (otherwise, they
1433-won't be translated in the admin interface). This means writing explicit
1434-``verbose_name`` and ``verbose_name_plural`` options in the ``Meta`` class,
1435-though, rather than relying on Django's default determination of
1436-``verbose_name`` and ``verbose_name_plural`` by looking at the model's class
1437-name::
1438-
1439-    from django.utils.translation import ugettext_lazy as _
1440-
1441-    class MyThing(models.Model):
1442-        name = models.CharField(_('name'), help_text=_('This is the help text'))
1443-
1444-        class Meta:
1445-            verbose_name = _('my thing')
1446-            verbose_name_plural = _('my things')
1447-
1448-Notes on model classes translation
1449-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1450-
1451-Your model classes may not only contain normal fields: you may have relations
1452-(with a ``ForeignKey`` field) or additional model methods you may use for
1453-columns in the Django admin site.
1454-
1455-If you have models with foreign keys and you use the Django admin site, you can
1456-provide translations for the relation itself by using the ``verbose_name``
1457-parameter on the ``ForeignKey`` object::
1458-
1459-    class MyThing(models.Model):
1460-        kind = models.ForeignKey(ThingKind, related_name='kinds',
1461-                                 verbose_name=_('kind'))
1462-
1463-As you would do for the ``verbose_name`` and ``verbose_name_plural`` settings of
1464-a model Meta class, you should provide a lowercase verbose name text for the
1465-relation as Django will automatically titlecase it when required.
1466-
1467-For model methods, you can provide translations to Django and the admin site
1468-with the ``short_description`` parameter set on the corresponding method::
1469-
1470-    class MyThing(models.Model):
1471-        kind = models.ForeignKey(ThingKind, related_name='kinds',
1472-                                 verbose_name=_('kind'))
1473-
1474-        def is_mouse(self):
1475-            return self.kind.type == MOUSE_TYPE
1476-        is_mouse.short_description = _('Is it a mouse?')
1477-
1478-As always with model classes translations, don't forget to use the lazy
1479-translation method!
1480-
1481 Working with lazy translation objects
1482 -------------------------------------
1483 
1484diff --git a/tests/modeltests/custom_pk/models.py b/tests/modeltests/custom_pk/models.py
1485--- a/tests/modeltests/custom_pk/models.py
1486+++ b/tests/modeltests/custom_pk/models.py
1487@@ -26,12 +26,14 @@
1488 class Business(models.Model):
1489     name = models.CharField(max_length=20, primary_key=True)
1490     employees = models.ManyToManyField(Employee)
1491-    class Meta:
1492-        verbose_name_plural = 'businesses'
1493 
1494     def __unicode__(self):
1495         return self.name
1496 
1497+    @classmethod
1498+    def verbose_names(cls, count=1):
1499+        return 'businesses'
1500+
1501 class Bar(models.Model):
1502     id = MyAutoField(primary_key=True, db_index=True)
1503 
1504diff --git a/tests/regressiontests/admin_util/models.py b/tests/regressiontests/admin_util/models.py
1505--- a/tests/regressiontests/admin_util/models.py
1506+++ b/tests/regressiontests/admin_util/models.py
1507@@ -34,5 +34,6 @@
1508     event = models.OneToOneField(Event)
1509     name = models.CharField(max_length=255)
1510 
1511-    class Meta:
1512-        verbose_name = "awesome guest"
1513+    @classmethod
1514+    def verbose_names(cls, count=1):
1515+        return "awesome guest"
1516diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
1517--- a/tests/regressiontests/admin_views/models.py
1518+++ b/tests/regressiontests/admin_views/models.py
1519@@ -63,9 +63,10 @@
1520     def __unicode__(self):
1521         return self.title
1522 
1523-    class Meta:
1524+    @classmethod
1525+    def verbose_names(cls, count=1):
1526         # Use a utf-8 bytestring to ensure it works (see #11710)
1527-        verbose_name = '¿Chapter?'
1528+        return '¿Chapter?'
1529 
1530 
1531 class ChapterXtra1(models.Model):
1532@@ -538,13 +539,13 @@
1533     age = models.PositiveIntegerField()
1534     is_employee = models.NullBooleanField()
1535 
1536-class PrePopulatedPostLargeSlug(models.Model):
1537-    """
1538-    Regression test for #15938: a large max_length for the slugfield must not
1539-    be localized in prepopulated_fields_js.html or it might end up breaking
1540-    the javascript (ie, using THOUSAND_SEPARATOR ends up with maxLength=1,000)
1541-    """
1542-    title = models.CharField(max_length=100)
1543-    published = models.BooleanField()
1544+class PrePopulatedPostLargeSlug(models.Model):
1545+    """
1546+    Regression test for #15938: a large max_length for the slugfield must not
1547+    be localized in prepopulated_fields_js.html or it might end up breaking
1548+    the javascript (ie, using THOUSAND_SEPARATOR ends up with maxLength=1,000)
1549+    """
1550+    title = models.CharField(max_length=100)
1551+    published = models.BooleanField()
1552     slug = models.SlugField(max_length=1000)
1553-   
1554+
1555diff --git a/tests/regressiontests/backends/models.py b/tests/regressiontests/backends/models.py
1556--- a/tests/regressiontests/backends/models.py
1557+++ b/tests/regressiontests/backends/models.py
1558@@ -28,14 +28,16 @@
1559 # Until #13711 is fixed, this test can't be run under MySQL.
1560 if connection.features.supports_long_model_names:
1561     class VeryLongModelNameZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ(models.Model):
1562-        class Meta:
1563-            # We need to use a short actual table name or
1564-            # we hit issue #8548 which we're not testing!
1565-            verbose_name = 'model_with_long_table_name'
1566         primary_key_is_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.AutoField(primary_key=True)
1567         charfield_is_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.CharField(max_length=100)
1568         m2m_also_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.ManyToManyField(Person,blank=True)
1569 
1570+        # We need to use a short actual verbose name or
1571+        # we hit issue #8548 which we're not testing!
1572+        @classmethod
1573+        def verbose_names(cls, count=1):
1574+            return 'model_with_long_table_name'
1575+
1576 
1577 class Tag(models.Model):
1578     name = models.CharField(max_length=30)
1579diff --git a/tests/regressiontests/generic_views/models.py b/tests/regressiontests/generic_views/models.py
1580--- a/tests/regressiontests/generic_views/models.py
1581+++ b/tests/regressiontests/generic_views/models.py
1582@@ -1,13 +1,11 @@
1583 from django.db import models
1584-
1585+from django.utils.translation import ungettext_lazy
1586 
1587 class Artist(models.Model):
1588     name = models.CharField(max_length=100)
1589 
1590     class Meta:
1591         ordering = ['name']
1592-        verbose_name = 'professional artist'
1593-        verbose_name_plural = 'professional artists'
1594 
1595     def __unicode__(self):
1596         return self.name
1597@@ -16,6 +14,10 @@
1598     def get_absolute_url(self):
1599         return ('artist_detail', (), {'pk': self.id})
1600 
1601+    @classmethod
1602+    def verbose_names(cls, count=1):
1603+        return ungettext_lazy('professional artist', 'professional artists', count)
1604+
1605 class Author(models.Model):
1606     name = models.CharField(max_length=100)
1607     slug = models.SlugField()
1608diff --git a/tests/regressiontests/i18n/models.py b/tests/regressiontests/i18n/models.py
1609--- a/tests/regressiontests/i18n/models.py
1610+++ b/tests/regressiontests/i18n/models.py
1611@@ -13,5 +13,6 @@
1612     cents_payed = models.DecimalField(max_digits=4, decimal_places=2)
1613     products_delivered = models.IntegerField()
1614 
1615-    class Meta:
1616-        verbose_name = _('Company')
1617\ No newline at end of file
1618+    @classmethod
1619+    def verbose_names(cls, count=1):
1620+        return _('Company')
1621diff --git a/tests/regressiontests/model_inheritance_regress/models.py b/tests/regressiontests/model_inheritance_regress/models.py
1622--- a/tests/regressiontests/model_inheritance_regress/models.py
1623+++ b/tests/regressiontests/model_inheritance_regress/models.py
1624@@ -108,7 +108,10 @@
1625 
1626     class Meta:
1627         abstract = True
1628-        verbose_name_plural = u'Audits'
1629+
1630+    @classmethod
1631+    def verbose_names(cls, count=1):
1632+        return u'Audits'
1633 
1634 class CertificationAudit(AuditBase):
1635     class Meta(AuditBase.Meta):
1636diff --git a/tests/regressiontests/model_regress/models.py b/tests/regressiontests/model_regress/models.py
1637--- a/tests/regressiontests/model_regress/models.py
1638+++ b/tests/regressiontests/model_regress/models.py
1639@@ -16,12 +16,15 @@
1640 
1641     class Meta:
1642         ordering = ('pub_date','headline')
1643-        # A utf-8 verbose name (Ångström's Articles) to test they are valid.
1644-        verbose_name = "\xc3\x85ngstr\xc3\xb6m's Articles"
1645 
1646     def __unicode__(self):
1647         return self.headline
1648 
1649+    @classmethod
1650+    def verbose_names(cls, count=1):
1651+        # An utf-8 verbose name (Ångström's Articles) to test they are valid.
1652+        return "\xc3\x85ngstr\xc3\xb6m's Articles"
1653+
1654 class Movie(models.Model):
1655     #5218: Test models with non-default primary keys / AutoFields
1656     movie_id = models.AutoField(primary_key=True)
1657diff --git a/tests/regressiontests/verbose_names/__init__.py b/tests/regressiontests/verbose_names/__init__.py
1658new file mode 100644
1659diff --git a/tests/regressiontests/verbose_names/locale/es_AR/LC_MESSAGES/django.mo b/tests/regressiontests/verbose_names/locale/es_AR/LC_MESSAGES/django.mo
1660new file mode 100644
1661index 0000000000000000000000000000000000000000..5a0d607200b2b05b66bf95085b76d736061c03bf
1662GIT binary patch
1663literal 1052
1664zc$|%q-EPw`7=|-CeiY6)<<E!{KzQUwz~r^4p-rb~NmsS$jyUlWlVwWn%65TX0Nep*
1665zoFQ=^a0EB8b1njjFH5R2Xg939d2Ky!{C%-cZft%uU|a*;0=)uV2YoWC@f~y*^aIoe
1666z{RAP<qm3%RSNjq8ZSeQCe+EAWeXaT9hG8_pgGROA2k@KVUuypbeh2)d_Fv!|bzkMU
1667z1ZvdttDsHLm3n=-e8rM-RZvY23x@jaEmnFCvx!Jqf$rNEaOYgE{v+U?4HPV+$$U-u
1668zv(Q-&|1JC%b&Y`($1Kq}Nbv!aDi@rff!Epdc71f<4@bd%-yE{3kQzrS%TxSvmMI(y
1669zg3Q5wj60H14Ikky)r=s=vTY2H^Bma@fr<6ZvLJ|u>`ks#=bjSF!j=oUkSdKA?Fvay
1670zZybiG??pap`jHoeZLixM`lHbci1KVkGlpYECj^CFe|KEA+zi8Zk#7l^Ei=$+6h!{=
1671z|LiTx><F$IhjQ~N1T=f6pG*sy^Bt5NQK^`2jpH8PUgpZSUNeb(o`^K(8A02Jxi$yI
1672zOwt1Pgq$dX`1EwCTFwrtrmY7Y-D@HHp=m_rp^MYAms}t%X*x^t@>bx2&w)-2)q<)&
1673bu)3JLi_Oc+PuCP)!It}0aBbO@*fo9wrIRVG
1674
1675diff --git a/tests/regressiontests/verbose_names/locale/es_AR/LC_MESSAGES/django.po b/tests/regressiontests/verbose_names/locale/es_AR/LC_MESSAGES/django.po
1676new file mode 100644
1677--- /dev/null
1678+++ b/tests/regressiontests/verbose_names/locale/es_AR/LC_MESSAGES/django.po
1679@@ -0,0 +1,41 @@
1680+# SOME DESCRIPTIVE TITLE.
1681+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
1682+# This file is distributed under the same license as the PACKAGE package.
1683+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
1684+#
1685+msgid ""
1686+msgstr ""
1687+"Project-Id-Version: PACKAGE VERSION\n"
1688+"Report-Msgid-Bugs-To: \n"
1689+"POT-Creation-Date: 2011-11-27 12:11-0600\n"
1690+"PO-Revision-Date: 2011-11-27 15:00-0300\n"
1691+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1692+"Language-Team: LANGUAGE <LL@li.org>\n"
1693+"Language: \n"
1694+"MIME-Version: 1.0\n"
1695+"Content-Type: text/plain; charset=UTF-8\n"
1696+"Content-Transfer-Encoding: 8bit\n"
1697+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
1698+
1699+msgid "Translatable legacy model #1"
1700+msgstr "Modelo legado traducible #1"
1701+
1702+msgid "Translatable legacy model #2"
1703+msgstr "Modelo legado traducible #2"
1704+
1705+msgid "Translatable legacy models #2"
1706+msgstr "Modelos legados traducibles #2"
1707+
1708+msgid "Translatable legacy models #3"
1709+msgstr "Modelos legados traducibles #3"
1710+
1711+msgid "Translatable New-style model #1"
1712+msgstr "Modelo moderno traducible #1"
1713+
1714+msgid "Translatable New-style model #2"
1715+msgid_plural "Translatable New-style models #2"
1716+msgstr[0] "Modelo moderno traducible #2"
1717+msgstr[1] "Modelos modernos traducibles #2"
1718+
1719+msgid "Translatable New-style models #3"
1720+msgstr "Modelos modernos traducibles #3"
1721diff --git a/tests/regressiontests/verbose_names/models.py b/tests/regressiontests/verbose_names/models.py
1722new file mode 100644
1723--- /dev/null
1724+++ b/tests/regressiontests/verbose_names/models.py
1725@@ -0,0 +1,95 @@
1726+from django.db import models
1727+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
1728+
1729+
1730+class NoMeta(models.Model):
1731+    name = models.CharField(max_length=20)
1732+
1733+class LegacyOnlyVerboseName(models.Model):
1734+    name = models.CharField(max_length=20)
1735+
1736+    class Meta:
1737+        verbose_name = 'Legacy model #1'
1738+
1739+class LegacyBothVNames(models.Model):
1740+    name = models.CharField(max_length=20)
1741+
1742+    class Meta:
1743+        verbose_name = 'Legacy model #2'
1744+        verbose_name_plural = 'Legacy models #2'
1745+
1746+class LegacyOnlyVerboseNamePlural(models.Model):
1747+    name = models.CharField(max_length=20)
1748+
1749+    class Meta:
1750+        verbose_name_plural = 'Legacy models #3'
1751+
1752+class LegacyOnlyVerbosenameIntl(models.Model):
1753+    name = models.CharField(max_length=20)
1754+
1755+    class Meta:
1756+        verbose_name = _('Translatable legacy model #1')
1757+
1758+class LegacyBothIntl(models.Model):
1759+    name = models.CharField(max_length=20)
1760+
1761+    class Meta:
1762+        verbose_name = _('Translatable legacy model #2')
1763+        verbose_name_plural = _('Translatable legacy models #2')
1764+
1765+class LegacyPluralIntl(models.Model):
1766+    name = models.CharField(max_length=20)
1767+
1768+    class Meta:
1769+        verbose_name_plural = _('Translatable legacy models #3')
1770+
1771+# === Models using new classmethod syntax for verbose names ==========
1772+
1773+class NewstyleSingular(models.Model):
1774+    name = models.CharField(max_length=20)
1775+
1776+    @classmethod
1777+    def verbose_names(cls, count=1):
1778+        if count == 1:
1779+            return 'New-style model #1'
1780+
1781+class NewstyleBoth(models.Model):
1782+    name = models.CharField(max_length=20)
1783+
1784+    @classmethod
1785+    def verbose_names(cls, count=1):
1786+        if count == 1:
1787+            return 'New-style model #2'
1788+        else:
1789+            return 'New-style models #2'
1790+
1791+class NewstylePlural(models.Model):
1792+    name = models.CharField(max_length=20)
1793+
1794+    @classmethod
1795+    def verbose_names(cls, count=1):
1796+        if count != 1:
1797+            return 'New-style models #3'
1798+
1799+class NewstyleSingularIntl(models.Model):
1800+    name = models.CharField(max_length=20)
1801+
1802+    @classmethod
1803+    def verbose_names(cls, count=1):
1804+        if count == 1:
1805+            return _('Translatable New-style model #1')
1806+
1807+class NewstyleBothIntl(models.Model):
1808+    name = models.CharField(max_length=20)
1809+
1810+    @classmethod
1811+    def verbose_names(cls, count=1):
1812+        return ungettext_lazy('Translatable New-style model #2', 'Translatable New-style models #2', count)
1813+
1814+class NewstylePluralIntl(models.Model):
1815+    name = models.CharField(max_length=20)
1816+
1817+    @classmethod
1818+    def verbose_names(cls, count=1):
1819+        if count != 1:
1820+            return _('Translatable New-style models #3')
1821diff --git a/tests/regressiontests/verbose_names/tests.py b/tests/regressiontests/verbose_names/tests.py
1822new file mode 100644
1823--- /dev/null
1824+++ b/tests/regressiontests/verbose_names/tests.py
1825@@ -0,0 +1,246 @@
1826+from __future__ import absolute_import
1827+
1828+from django.utils.encoding import force_unicode
1829+from django.utils import translation
1830+from django.utils.unittest import TestCase
1831+
1832+from .models import (NoMeta, LegacyOnlyVerboseName, LegacyBothVNames,
1833+        LegacyOnlyVerboseNamePlural, LegacyOnlyVerbosenameIntl, LegacyBothIntl,
1834+        LegacyPluralIntl, NewstyleSingular, NewstyleBoth, NewstylePlural,
1835+        NewstyleSingularIntl, NewstyleBothIntl, NewstylePluralIntl)
1836+
1837+
1838+class LegacyVerboseNameNoI18NTests(TestCase):
1839+    """
1840+    Test we don't disrupt behavior associated with legacy
1841+    Meta.verbose_name{,_plural} attributes when translation isn't used.
1842+    """
1843+
1844+    def test_noi18n_no_meta_inner_class(self):
1845+        # A model without an inner Meta class
1846+        a = NoMeta.objects.create(name=u'Name')
1847+        self.assertEqual('no meta', NoMeta._meta.verbose_name)
1848+        self.assertEqual('no meta', a._meta.verbose_name)
1849+        # Automatically generated plural form, can be bogus (note the arbitrary
1850+        # 's' tucked at the end)
1851+        self.assertEqual('no metas', force_unicode(NoMeta._meta.verbose_name_plural))
1852+        self.assertEqual('no metas', force_unicode(a._meta.verbose_name_plural))
1853+
1854+    def test_noi18n_only_verbose_name_option(self):
1855+        a = LegacyOnlyVerboseName.objects.create(name=u'Name')
1856+        # The verbose_name we specified
1857+        self.assertEqual('Legacy model #1', LegacyOnlyVerboseName._meta.verbose_name)
1858+        self.assertEqual('Legacy model #1', a._meta.verbose_name)
1859+        # Automatically generated plural form, can be bogus (note the arbitrary
1860+        # 's' tucked at the end)
1861+        self.assertEqual('Legacy model #1s', force_unicode(LegacyOnlyVerboseName._meta.verbose_name_plural))
1862+        self.assertEqual('Legacy model #1s', force_unicode(a._meta.verbose_name_plural))
1863+
1864+    def test_noi18n_both_verbose_name_options(self):
1865+        b = LegacyBothVNames.objects.create(name=u'Name')
1866+        # The verbose_name we specified
1867+        self.assertEqual('Legacy model #2', LegacyBothVNames._meta.verbose_name)
1868+        self.assertEqual('Legacy model #2', b._meta.verbose_name)
1869+        # The verbose_name_plural we specified
1870+        self.assertEqual('Legacy models #2', LegacyBothVNames._meta.verbose_name_plural)
1871+        self.assertEqual('Legacy models #2', b._meta.verbose_name_plural)
1872+
1873+    def test_noi18n_only_verbose_name_plural_option(self):
1874+        c = LegacyOnlyVerboseNamePlural.objects.create(name=u'Name')
1875+        # Verbose name automatically generated from the class name
1876+        self.assertEqual('legacy only verbose name plural', LegacyOnlyVerboseNamePlural._meta.verbose_name)
1877+        self.assertEqual('legacy only verbose name plural', c._meta.verbose_name)
1878+        # The verbose_name_plural we specified
1879+        self.assertEqual('Legacy models #3', LegacyOnlyVerboseNamePlural._meta.verbose_name_plural)
1880+        self.assertEqual('Legacy models #3', c._meta.verbose_name_plural)
1881+
1882+
1883+class LegacyVerboseNameI18NTests(TestCase):
1884+    """
1885+    Test we don't disrupt behavior associated with legacy
1886+    Meta.verbose_name{,_plural} attributes when translation is used.
1887+    """
1888+
1889+    def setUp(self):
1890+        translation.activate('es-ar')
1891+
1892+    def tearDown(self):
1893+        translation.deactivate()
1894+
1895+    def test_i18n_no_meta_inner_class(self):
1896+        # A model without an inner Meta class
1897+        a = NoMeta.objects.create(name=u'Name')
1898+        self.assertEqual('no meta', NoMeta._meta.verbose_name)
1899+        self.assertEqual('no meta', a._meta.verbose_name)
1900+        # Automatically generated plural form, can be bogus (note the arbitrary
1901+        # 's' tucked at the end)
1902+        self.assertEqual('no metas', force_unicode(NoMeta._meta.verbose_name_plural))
1903+        self.assertEqual('no metas', force_unicode(a._meta.verbose_name_plural))
1904+
1905+    def test_i18n_only_verbose_name_option(self):
1906+        a = LegacyOnlyVerbosenameIntl.objects.create(name=u'Name')
1907+        # The verbose_name we specified
1908+        self.assertEqual('Modelo legado traducible #1', force_unicode(LegacyOnlyVerbosenameIntl._meta.verbose_name))
1909+        self.assertEqual('Modelo legado traducible #1', a._meta.verbose_name)
1910+        # Automatically generated plural form, can be bogus (note the arbitrary
1911+        # 's' tucked at the end)
1912+        self.assertEqual('Modelo legado traducible #1s', force_unicode(LegacyOnlyVerbosenameIntl._meta.verbose_name_plural))
1913+        self.assertEqual('Modelo legado traducible #1s', force_unicode(a._meta.verbose_name_plural))
1914+
1915+    def test_i18n_both_verbose_name_options(self):
1916+        a = LegacyBothIntl.objects.create(name=u'Name')
1917+        # The verbose_name we specified
1918+        self.assertEqual('Modelo legado traducible #2', LegacyBothIntl._meta.verbose_name)
1919+        self.assertEqual('Modelo legado traducible #2', a._meta.verbose_name)
1920+        # The verbose_name_plural we specified
1921+        self.assertEqual('Modelos legados traducibles #2', LegacyBothIntl._meta.verbose_name_plural)
1922+        self.assertEqual('Modelos legados traducibles #2', a._meta.verbose_name_plural)
1923+
1924+    def test_i18n_only_verbose_name_plural_option(self):
1925+        a = LegacyPluralIntl.objects.create(name=u'Name')
1926+        # Verbose name automatically generated from the class name
1927+        self.assertEqual('legacy plural intl', LegacyPluralIntl._meta.verbose_name)
1928+        self.assertEqual('legacy plural intl', a._meta.verbose_name)
1929+        # The verbose_name_plural we specified
1930+        self.assertEqual('Modelos legados traducibles #3', LegacyPluralIntl._meta.verbose_name_plural)
1931+        self.assertEqual('Modelos legados traducibles #3', a._meta.verbose_name_plural)
1932+
1933+
1934+class VerboseNameNoI18NTests(TestCase):
1935+    """
1936+    Test new verbose_names() model classmethod behavior when translation isn't
1937+    used.
1938+    """
1939+
1940+    def test_backward_compatibility(self):
1941+        """
1942+        Test backward compatibility with legacy Meta.verbose_name{,_plural}
1943+        attributes.
1944+        """
1945+        a = NewstyleSingular.objects.create(name=u'Name')
1946+        # The verbose_name derived from the verbose_names() method we specified
1947+        self.assertEqual('New-style model #1', NewstyleSingular._meta.verbose_name)
1948+        self.assertEqual('New-style model #1', a._meta.verbose_name)
1949+        # Automatically generated plural form, can be bogus (note the arbitrary
1950+        # 's' tucked at the end)
1951+        self.assertEqual('New-style model #1s', force_unicode(NewstyleSingular._meta.verbose_name_plural))
1952+        self.assertEqual('New-style model #1s', force_unicode(a._meta.verbose_name_plural))
1953+
1954+        b = NewstyleBoth.objects.create(name=u'Name')
1955+        # The verbose_name derived from the verbose_names() we specified
1956+        self.assertEqual('New-style model #2', NewstyleBoth._meta.verbose_name)
1957+        self.assertEqual('New-style model #2', b._meta.verbose_name)
1958+        # The verbose_name_plural derived from the verbose_names() method we
1959+        # specified
1960+        self.assertEqual('New-style models #2', NewstyleBoth._meta.verbose_name_plural)
1961+        self.assertEqual('New-style models #2', b._meta.verbose_name_plural)
1962+
1963+        c = NewstylePlural.objects.create(name=u'Name')
1964+        # Verbose name automatically generated from the class name
1965+        self.assertEqual('newstyle plural', NewstylePlural._meta.verbose_name)
1966+        self.assertEqual('newstyle plural', c._meta.verbose_name)
1967+        # The verbose_name_plural derived from the verbose_names() method we
1968+        # specified
1969+        self.assertEqual('New-style models #3', NewstylePlural._meta.verbose_name_plural)
1970+        self.assertEqual('New-style models #3', c._meta.verbose_name_plural)
1971+
1972+    def test_new_behavior(self):
1973+        """
1974+        Test sanity of new verbose_names() model classmethod.
1975+        """
1976+        a = NewstyleSingular.objects.create(name=u'Name')
1977+        self.assertEqual('New-style model #1', NewstyleSingular._meta.get_verbose_name())
1978+        self.assertEqual('New-style model #1', a._meta.get_verbose_name())
1979+        # Fallback get_verbose_name() implementation, its return value
1980+        # can be bogus (note the arbitrary 's' tucked at the end)
1981+        self.assertEqual('New-style model #1s', force_unicode(NewstyleSingular._meta.get_verbose_name(0)))
1982+        self.assertEqual('New-style model #1s', force_unicode(a._meta.get_verbose_name(0)))
1983+
1984+        b = NewstyleBoth.objects.create(name=u'Name')
1985+        self.assertEqual('New-style model #2', NewstyleBoth._meta.get_verbose_name())
1986+        self.assertEqual('New-style model #2', b._meta.get_verbose_name())
1987+
1988+        self.assertEqual('New-style models #2', NewstyleBoth._meta.get_verbose_name(0))
1989+        self.assertEqual('New-style models #2', b._meta.get_verbose_name(0))
1990+
1991+        c = NewstylePlural.objects.create(name=u'Name')
1992+        # Fallback get_verbose_name() implementation: Returns a value
1993+        # automatically generated from the class name
1994+        self.assertEqual('newstyle plural', NewstylePlural._meta.get_verbose_name())
1995+        self.assertEqual('newstyle plural', c._meta.get_verbose_name())
1996+
1997+        self.assertEqual('New-style models #3', NewstylePlural._meta.get_verbose_name(0))
1998+        self.assertEqual('New-style models #3', c._meta.get_verbose_name(0))
1999+
2000+
2001+class VerboseNameI18NTests(TestCase):
2002+    """
2003+    Test new verbose_names() model classmethod behavior when translation is
2004+    used.
2005+    """
2006+
2007+    def setUp(self):
2008+        translation.activate('es-ar')
2009+
2010+    def tearDown(self):
2011+        translation.deactivate()
2012+
2013+    def test_backward_compatibility(self):
2014+        """
2015+        Test backward compatibility with legacy Meta.verbose_name{,_plural}
2016+        attributes.
2017+        """
2018+        a = NewstyleSingularIntl.objects.create(name=u'Name')
2019+        # The verbose_name derived from the verbose_names() method we specified
2020+        self.assertEqual('Modelo moderno traducible #1', NewstyleSingularIntl._meta.verbose_name)
2021+        self.assertEqual('Modelo moderno traducible #1', a._meta.verbose_name)
2022+        # Automatically generated plural form, can be bogus (note the arbitrary
2023+        # 's' tucked at the end)
2024+        self.assertEqual('Modelo moderno traducible #1s', force_unicode(NewstyleSingularIntl._meta.verbose_name_plural))
2025+        self.assertEqual('Modelo moderno traducible #1s', force_unicode(a._meta.verbose_name_plural))
2026+
2027+        b = NewstyleBothIntl.objects.create(name=u'Name')
2028+        # The verbose_name derived from the verbose_names() we specified
2029+        self.assertEqual('Modelo moderno traducible #2', force_unicode(NewstyleBothIntl._meta.verbose_name))
2030+        self.assertEqual('Modelo moderno traducible #2', b._meta.verbose_name)
2031+        # The verbose_name_plural derived from the verbose_names() method we
2032+        # specified
2033+        self.assertEqual('Modelos modernos traducibles #2', NewstyleBothIntl._meta.verbose_name_plural)
2034+        self.assertEqual('Modelos modernos traducibles #2', b._meta.verbose_name_plural)
2035+
2036+        c = NewstylePluralIntl.objects.create(name=u'Name')
2037+        # Verbose name automatically generated from the class name -- untranslatable
2038+        self.assertEqual('newstyle plural intl', NewstylePluralIntl._meta.verbose_name)
2039+        self.assertEqual('newstyle plural intl', c._meta.verbose_name)
2040+        # The verbose_name_plural derived from the verbose_names() method we
2041+        # specified
2042+        self.assertEqual('Modelos modernos traducibles #3', NewstylePluralIntl._meta.verbose_name_plural)
2043+        self.assertEqual('Modelos modernos traducibles #3', c._meta.verbose_name_plural)
2044+
2045+    def test_new_behavior(self):
2046+        """
2047+        Test sanity of new verbose_names() model classmethod.
2048+        """
2049+        a = NewstyleSingularIntl.objects.create(name=u'Name')
2050+        self.assertEqual('Modelo moderno traducible #1', NewstyleSingularIntl._meta.get_verbose_name())
2051+        self.assertEqual('Modelo moderno traducible #1', a._meta.get_verbose_name())
2052+        # Fallback get_verbose_name() implementation, its return value
2053+        # can be bogus (note the arbitrary 's' tucked at the end)
2054+        self.assertEqual('Modelo moderno traducible #1s', force_unicode(NewstyleSingularIntl._meta.get_verbose_name(0)))
2055+        self.assertEqual('Modelo moderno traducible #1s', force_unicode(a._meta.get_verbose_name(0)))
2056+
2057+        b = NewstyleBothIntl.objects.create(name=u'Name')
2058+        self.assertEqual('Modelo moderno traducible #2', NewstyleBothIntl._meta.get_verbose_name())
2059+        self.assertEqual('Modelo moderno traducible #2', b._meta.get_verbose_name())
2060+
2061+        self.assertEqual('Modelos modernos traducibles #2', NewstyleBothIntl._meta.get_verbose_name(0))
2062+        self.assertEqual('Modelos modernos traducibles #2', b._meta.get_verbose_name(0))
2063+
2064+        c = NewstylePluralIntl.objects.create(name=u'Name')
2065+        # Fallback get_verbose_name() implementation: Returns a value
2066+        # automatically generated from the class name -- untranslatable
2067+        self.assertEqual('newstyle plural intl', NewstylePluralIntl._meta.get_verbose_name())
2068+        self.assertEqual('newstyle plural intl', c._meta.get_verbose_name())
2069+
2070+        self.assertEqual('Modelos modernos traducibles #3', NewstylePluralIntl._meta.get_verbose_name(0))
2071+        self.assertEqual('Modelos modernos traducibles #3', c._meta.get_verbose_name(0))