diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py
--- a/django/contrib/admin/actions.py
+++ b/django/contrib/admin/actions.py
@@ -20,14 +20,15 @@
 
     Next, it delets all selected objects and redirects back to the change list.
     """
-    opts = modeladmin.model._meta
+    model = modeladmin.model
+    opts = model._meta
     app_label = opts.app_label
 
     # Check that the user has delete permission for the actual model
     if not modeladmin.has_delete_permission(request):
         raise PermissionDenied
 
-    using = router.db_for_write(modeladmin.model)
+    using = router.db_for_write(model)
 
     # Populate deletable_objects, a data structure of all related objects that
     # will also be deleted.
@@ -46,15 +47,12 @@
                 modeladmin.log_deletion(request, obj, obj_display)
             queryset.delete()
             modeladmin.message_user(request, _("Successfully deleted %(count)d %(items)s.") % {
-                "count": n, "items": model_ngettext(modeladmin.opts, n)
+                "count": n, "items": model_ngettext(model, n)
             })
         # Return None to display the change list page again.
         return None
 
-    if len(queryset) == 1:
-        objects_name = force_unicode(opts.verbose_name)
-    else:
-        objects_name = force_unicode(opts.verbose_name_plural)
+    objects_name = force_unicode(opts.get_verbose_name(len(queryset)))
 
     if perms_needed or protected:
         title = _("Cannot delete %(name)s") % {"name": objects_name}
diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py
--- a/django/contrib/admin/filters.py
+++ b/django/contrib/admin/filters.py
@@ -150,7 +150,7 @@
         if hasattr(field, 'verbose_name'):
             self.lookup_title = field.verbose_name
         else:
-            self.lookup_title = other_model._meta.verbose_name
+            self.lookup_title = other_model._meta.get_verbose_name()
         rel_name = other_model._meta.pk.name
         self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name)
         self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path)
diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py
--- a/django/contrib/admin/models.py
+++ b/django/contrib/admin/models.py
@@ -2,7 +2,7 @@
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.auth.models import User
 from django.contrib.admin.util import quote
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
 from django.utils.encoding import smart_unicode
 from django.utils.safestring import mark_safe
 
@@ -27,8 +27,6 @@
     objects = LogEntryManager()
 
     class Meta:
-        verbose_name = _('log entry')
-        verbose_name_plural = _('log entries')
         db_table = 'django_admin_log'
         ordering = ('-action_time',)
 
@@ -66,3 +64,7 @@
         if self.content_type and self.object_id:
             return mark_safe(u"%s/%s/%s/" % (self.content_type.app_label, self.content_type.model, quote(self.object_id)))
         return None
+
+    @classmethod
+    def verbose_names(cls, count=1):
+        return ungettext_lazy('log entry', 'log entries', count)
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -609,7 +609,7 @@
         """
         choices = [] + default_choices
         for func, name, description in self.get_actions(request).itervalues():
-            choice = (name, description % model_format_dict(self.opts))
+            choice = (name, description % model_format_dict(self.model))
             choices.append(choice)
         return choices
 
@@ -674,16 +674,16 @@
             for formset in formsets:
                 for added_object in formset.new_objects:
                     change_message.append(_('Added %(name)s "%(object)s".')
-                                          % {'name': force_unicode(added_object._meta.verbose_name),
+                                          % {'name': force_unicode(added_object._meta.get_verbose_name()),
                                              'object': force_unicode(added_object)})
                 for changed_object, changed_fields in formset.changed_objects:
                     change_message.append(_('Changed %(list)s for %(name)s "%(object)s".')
                                           % {'list': get_text_list(changed_fields, _('and')),
-                                             'name': force_unicode(changed_object._meta.verbose_name),
+                                             'name': force_unicode(changed_object._meta.get_verbose_name()),
                                              'object': force_unicode(changed_object)})
                 for deleted_object in formset.deleted_objects:
                     change_message.append(_('Deleted %(name)s "%(object)s".')
-                                          % {'name': force_unicode(deleted_object._meta.verbose_name),
+                                          % {'name': force_unicode(deleted_object._meta.get_verbose_name()),
                                              'object': force_unicode(deleted_object)})
         change_message = ' '.join(change_message)
         return change_message or _('No fields changed.')
@@ -769,7 +769,7 @@
         opts = obj._meta
         pk_value = obj._get_pk_val()
 
-        msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)}
+        msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.get_verbose_name()), 'obj': force_unicode(obj)}
         # Here, we distinguish between different save types by checking for
         # the presence of keys in request.POST.
         if "_continue" in request.POST:
@@ -785,7 +785,7 @@
                 # escape() calls force_unicode.
                 (escape(pk_value), escapejs(obj)))
         elif "_addanother" in request.POST:
-            self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
+            self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.get_verbose_name())))
             return HttpResponseRedirect(request.path)
         else:
             self.message_user(request, msg)
@@ -810,11 +810,11 @@
 
         # Handle proxy models automatically created by .only() or .defer().
         # Refs #14529
-        verbose_name = opts.verbose_name
+        verbose_name = opts.get_verbose_name()
         module_name = opts.module_name
         if obj._deferred:
             opts_ = opts.proxy_for_model._meta
-            verbose_name = opts_.verbose_name
+            verbose_name = opts_.get_verbose_name()
             module_name = opts_.module_name
 
         pk_value = obj._get_pk_val()
@@ -995,7 +995,7 @@
             media = media + inline_admin_formset.media
 
         context = {
-            'title': _('Add %s') % force_unicode(opts.verbose_name),
+            'title': _('Add %s') % force_unicode(opts.get_verbose_name()),
             'adminform': adminForm,
             'is_popup': "_popup" in request.REQUEST,
             'show_delete': False,
@@ -1020,7 +1020,7 @@
             raise PermissionDenied
 
         if obj is None:
-            raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
+            raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.get_verbose_name()), 'key': escape(object_id)})
 
         if request.method == 'POST' and "_saveasnew" in request.POST:
             return self.add_view(request, form_url=reverse('admin:%s_%s_add' %
@@ -1086,7 +1086,7 @@
             media = media + inline_admin_formset.media
 
         context = {
-            'title': _('Change %s') % force_unicode(opts.verbose_name),
+            'title': _('Change %s') % force_unicode(opts.get_verbose_name()),
             'adminform': adminForm,
             'object_id': object_id,
             'original': obj,
@@ -1194,15 +1194,11 @@
                         changecount += 1
 
                 if changecount:
-                    if changecount == 1:
-                        name = force_unicode(opts.verbose_name)
-                    else:
-                        name = force_unicode(opts.verbose_name_plural)
+                    name = force_unicode(opts.get_verbose_name(changecount))
                     msg = ungettext("%(count)s %(name)s was changed successfully.",
                                     "%(count)s %(name)s were changed successfully.",
                                     changecount) % {'count': changecount,
-                                                    'name': name,
-                                                    'obj': force_unicode(obj)}
+                                                    'name': name}
                     self.message_user(request, msg)
 
                 return HttpResponseRedirect(request.get_full_path())
@@ -1229,7 +1225,7 @@
             'All %(total_count)s selected', cl.result_count)
 
         context = {
-            'module_name': force_unicode(opts.verbose_name_plural),
+            'module_name': force_unicode(opts.get_verbose_name(0)),
             'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(cl.result_list)},
             'selection_note_all': selection_note_all % {'total_count': cl.result_count},
             'title': cl.title,
@@ -1255,7 +1251,8 @@
     @transaction.commit_on_success
     def delete_view(self, request, object_id, extra_context=None):
         "The 'delete' admin view for this model."
-        opts = self.model._meta
+        model = self.model
+        opts = model._meta
         app_label = opts.app_label
 
         obj = self.get_object(request, unquote(object_id))
@@ -1264,9 +1261,9 @@
             raise PermissionDenied
 
         if obj is None:
-            raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
+            raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.get_verbose_name()), 'key': escape(object_id)})
 
-        using = router.db_for_write(self.model)
+        using = router.db_for_write(model)
 
         # Populate deleted_objects, a data structure of all related objects that
         # will also be deleted.
@@ -1280,7 +1277,7 @@
             self.log_deletion(request, obj, obj_display)
             self.delete_model(request, obj)
 
-            self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj_display)})
+            self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.get_verbose_name()), 'obj': force_unicode(obj_display)})
 
             if not self.has_change_permission(request, None):
                 return HttpResponseRedirect(reverse('admin:index',
@@ -1289,7 +1286,7 @@
                                         (opts.app_label, opts.module_name),
                                         current_app=self.admin_site.name))
 
-        object_name = force_unicode(opts.verbose_name)
+        object_name = force_unicode(opts.get_verbose_name())
 
         if perms_needed or protected:
             title = _("Cannot delete %(name)s") % {"name": object_name}
@@ -1329,7 +1326,7 @@
         context = {
             'title': _('Change history: %s') % force_unicode(obj),
             'action_list': action_list,
-            'module_name': capfirst(force_unicode(opts.verbose_name_plural)),
+            'module_name': capfirst(force_unicode(opts.get_verbose_name(0))),
             'object': obj,
             'app_label': app_label,
             'opts': opts,
@@ -1365,9 +1362,9 @@
         self.opts = self.model._meta
         super(InlineModelAdmin, self).__init__()
         if self.verbose_name is None:
-            self.verbose_name = self.model._meta.verbose_name
+            self.verbose_name = self.model._meta.get_verbose_name()
         if self.verbose_name_plural is None:
-            self.verbose_name_plural = self.model._meta.verbose_name_plural
+            self.verbose_name_plural = self.model._meta.get_verbose_name(0)
 
     @property
     def media(self):
diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
--- a/django/contrib/admin/sites.py
+++ b/django/contrib/admin/sites.py
@@ -341,7 +341,7 @@
                 if True in perms.values():
                     info = (app_label, model._meta.module_name)
                     model_dict = {
-                        'name': capfirst(model._meta.verbose_name_plural),
+                        'name': capfirst(model._meta.get_verbose_name(0)),
                         'admin_url': reverse('admin:%s_%s_changelist' % info, current_app=self.name),
                         'add_url': reverse('admin:%s_%s_add' % info, current_app=self.name),
                         'perms': perms,
@@ -387,7 +387,7 @@
                     if True in perms.values():
                         info = (app_label, model._meta.module_name)
                         model_dict = {
-                            'name': capfirst(model._meta.verbose_name_plural),
+                            'name': capfirst(model._meta.get_verbose_name(0)),
                             'admin_url': reverse('admin:%s_%s_changelist' % info, current_app=self.name),
                             'add_url': reverse('admin:%s_%s_add' % info, current_app=self.name),
                             'perms': perms,
diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
--- a/django/contrib/admin/util.py
+++ b/django/contrib/admin/util.py
@@ -90,16 +90,16 @@
             p = '%s.%s' % (opts.app_label,
                            opts.get_delete_permission())
             if not user.has_perm(p):
-                perms_needed.add(opts.verbose_name)
+                perms_needed.add(opts.get_verbose_name())
             # Display a link to the admin page.
             return mark_safe(u'%s: <a href="%s">%s</a>' %
-                             (escape(capfirst(opts.verbose_name)),
+                             (escape(capfirst(opts.get_verbose_name())),
                               admin_url,
                               escape(obj)))
         else:
             # Don't display link to edit, because it either has no
             # admin or is edited inline.
-            return u'%s: %s' % (capfirst(opts.verbose_name),
+            return u'%s: %s' % (capfirst(opts.get_verbose_name()),
                                 force_unicode(obj))
 
     to_delete = collector.nested(format_callback)
@@ -165,7 +165,8 @@
     Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
     typically for use with string formatting.
 
-    `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
+    `obj` may be a `Model` instance, `Model` subclass, a `QuerySet` instance, or
+    a `django.db.models.Options` instance.
 
     """
     if isinstance(obj, (models.Model, models.base.ModelBase)):
@@ -175,8 +176,8 @@
     else:
         opts = obj
     return {
-        'verbose_name': force_unicode(opts.verbose_name),
-        'verbose_name_plural': force_unicode(opts.verbose_name_plural)
+        'verbose_name': force_unicode(opts.get_verbose_name()),
+        'verbose_name_plural': force_unicode(opts.get_verbose_name(0))
     }
 
 
@@ -194,9 +195,12 @@
         if n is None:
             n = obj.count()
         obj = obj.model
-    d = model_format_dict(obj)
-    singular, plural = d["verbose_name"], d["verbose_name_plural"]
-    return ungettext(singular, plural, n or 0)
+    else:
+        if n is None:
+            n = 0
+    singular = obj._meta.get_verbose_name(1)
+    plural = obj._meta.get_verbose_name(n)
+    return ungettext(singular, plural, n)
 
 
 def lookup_field(name, obj, model_admin=None):
@@ -229,7 +233,7 @@
 def label_for_field(name, model, model_admin=None, return_attr=False):
     """
     Returns a sensible label for a field name. The name can be a callable or the
-    name of an object attributes, as well as a genuine fields. If return_attr is
+    name of an object attribute, as well as a genuine field. If return_attr is
     True, the resolved attribute (which could be a callable) is also returned.
     This will be None if (and only if) the name refers to a field.
     """
@@ -237,15 +241,15 @@
     try:
         field = model._meta.get_field_by_name(name)[0]
         if isinstance(field, RelatedObject):
-            label = field.opts.verbose_name
+            label = field.opts.get_verbose_name()
         else:
             label = field.verbose_name
     except models.FieldDoesNotExist:
         if name == "__unicode__":
-            label = force_unicode(model._meta.verbose_name)
+            label = force_unicode(model._meta.get_verbose_name())
             attr = unicode
         elif name == "__str__":
-            label = smart_str(model._meta.verbose_name)
+            label = smart_str(model._meta.get_verbose_name())
             attr = str
         else:
             if callable(name):
@@ -311,7 +315,7 @@
 
 
 def get_model_from_relation(field):
-    if isinstance(field, models.related.RelatedObject):
+    if isinstance(field, RelatedObject):
         return field.model
     elif getattr(field, 'rel'): # or isinstance?
         return field.rel.to
diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
--- a/django/contrib/admin/views/main.py
+++ b/django/contrib/admin/views/main.py
@@ -81,7 +81,7 @@
             title = ugettext('Select %s')
         else:
             title = ugettext('Select %s to change')
-        self.title = title % force_unicode(self.opts.verbose_name)
+        self.title = title % force_unicode(self.opts.get_verbose_name())
         self.pk_attname = self.lookup_opts.pk.attname
 
     def get_filters(self, request, use_distinct=False):
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
--- a/django/contrib/auth/models.py
+++ b/django/contrib/auth/models.py
@@ -5,8 +5,8 @@
 from django.db import models
 from django.db.models.manager import EmptyManager
 from django.utils.encoding import smart_str
-from django.utils.translation import ugettext_lazy as _
 from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
 
 from django.contrib import auth
 from django.contrib.auth.signals import user_logged_in
@@ -54,8 +54,6 @@
     objects = PermissionManager()
 
     class Meta:
-        verbose_name = _('permission')
-        verbose_name_plural = _('permissions')
         unique_together = (('content_type', 'codename'),)
         ordering = ('content_type__app_label', 'content_type__model', 'codename')
 
@@ -69,6 +67,10 @@
         return (self.codename,) + self.content_type.natural_key()
     natural_key.dependencies = ['contenttypes.contenttype']
 
+    @classmethod
+    def verbose_names(cls, count=1):
+        return ungettext_lazy('permission', 'permissions', count)
+
 class Group(models.Model):
     """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.
 
@@ -79,13 +81,13 @@
     name = models.CharField(_('name'), max_length=80, unique=True)
     permissions = models.ManyToManyField(Permission, verbose_name=_('permissions'), blank=True)
 
-    class Meta:
-        verbose_name = _('group')
-        verbose_name_plural = _('groups')
-
     def __unicode__(self):
         return self.name
 
+    @classmethod
+    def verbose_names(cls, count=1):
+        return ungettext_lazy('group', 'groups', count)
+
 class UserManager(models.Manager):
     def create_user(self, username, email=None, password=None):
         """
@@ -188,10 +190,6 @@
     user_permissions = models.ManyToManyField(Permission, verbose_name=_('user permissions'), blank=True)
     objects = UserManager()
 
-    class Meta:
-        verbose_name = _('user')
-        verbose_name_plural = _('users')
-
     def __unicode__(self):
         return self.username
 
@@ -337,6 +335,10 @@
                 raise SiteProfileNotAvailable
         return self._profile_cache
 
+    @classmethod
+    def verbose_names(cls, count=1):
+        return ungettext_lazy('user', 'users', count)
+
 
 class AnonymousUser(object):
     id = None
diff --git a/django/contrib/comments/models.py b/django/contrib/comments/models.py
--- a/django/contrib/comments/models.py
+++ b/django/contrib/comments/models.py
@@ -5,8 +5,8 @@
 from django.contrib.sites.models import Site
 from django.db import models
 from django.core import urlresolvers
-from django.utils.translation import ugettext_lazy as _
 from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
 from django.conf import settings
 
 COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000)
@@ -73,8 +73,6 @@
         db_table = "django_comments"
         ordering = ('submit_date',)
         permissions = [("can_moderate", "Can moderate comments")]
-        verbose_name = _('comment')
-        verbose_name_plural = _('comments')
 
     def __unicode__(self):
         return "%s: %s..." % (self.name, self.comment[:50])
@@ -152,6 +150,10 @@
         }
         return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d
 
+    @classmethod
+    def verbose_names(cls, count=1):
+        return ungettext_lazy('comment', 'comments', count)
+
 class CommentFlag(models.Model):
     """
     Records a flag on a comment. This is intentionally flexible; right now, a
@@ -178,8 +180,6 @@
     class Meta:
         db_table = 'django_comment_flags'
         unique_together = [('user', 'comment', 'flag')]
-        verbose_name = _('comment flag')
-        verbose_name_plural = _('comment flags')
 
     def __unicode__(self):
         return "%s flag of comment ID %s by %s" % \
@@ -189,3 +189,7 @@
         if self.flag_date is None:
             self.flag_date = timezone.now()
         super(CommentFlag, self).save(*args, **kwargs)
+
+    @classmethod
+    def verbose_names(cls, count=1):
+        return ungettext_lazy('comment flag', 'comment flags', count)
diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py
--- a/django/contrib/contenttypes/models.py
+++ b/django/contrib/contenttypes/models.py
@@ -1,5 +1,5 @@
 from django.db import models
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
 from django.utils.encoding import smart_unicode, force_unicode
 
 class ContentTypeManager(models.Manager):
@@ -127,8 +127,6 @@
     objects = ContentTypeManager()
 
     class Meta:
-        verbose_name = _('content type')
-        verbose_name_plural = _('content types')
         db_table = 'django_content_type'
         ordering = ('name',)
         unique_together = (('app_label', 'model'),)
@@ -145,7 +143,7 @@
         if not model or self.name != model._meta.verbose_name_raw:
             return self.name
         else:
-            return force_unicode(model._meta.verbose_name)
+            return force_unicode(model._meta.get_verbose_name())
 
     def model_class(self):
         "Returns the Python model class for this type of content."
@@ -170,3 +168,7 @@
 
     def natural_key(self):
         return (self.app_label, self.model)
+
+    @classmethod
+    def verbose_names(cls, count=1):
+        return ungettext_lazy('content type', 'content types', count)
diff --git a/django/contrib/databrowse/datastructures.py b/django/contrib/databrowse/datastructures.py
--- a/django/contrib/databrowse/datastructures.py
+++ b/django/contrib/databrowse/datastructures.py
@@ -18,8 +18,8 @@
         self.site = site
         self.model = model
         self.model_list = site.registry.keys()
-        self.verbose_name = model._meta.verbose_name
-        self.verbose_name_plural = model._meta.verbose_name_plural
+        self.verbose_name = model._meta.get_verbose_name()
+        self.verbose_name_plural = model._meta.get_verbose_name(0)
 
     def __repr__(self):
         return '<EasyModel for %s>' % smart_str(self.model._meta.object_name)
diff --git a/django/contrib/flatpages/models.py b/django/contrib/flatpages/models.py
--- a/django/contrib/flatpages/models.py
+++ b/django/contrib/flatpages/models.py
@@ -1,6 +1,6 @@
 from django.db import models
 from django.contrib.sites.models import Site
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
 
 
 class FlatPage(models.Model):
@@ -15,8 +15,6 @@
 
     class Meta:
         db_table = 'django_flatpage'
-        verbose_name = _('flat page')
-        verbose_name_plural = _('flat pages')
         ordering = ('url',)
 
     def __unicode__(self):
@@ -24,3 +22,7 @@
 
     def get_absolute_url(self):
         return self.url
+
+    @classmethod
+    def verbose_names(cls, count=1):
+        return ungettext_lazy('flat page', 'flat pages', count)
diff --git a/django/contrib/redirects/models.py b/django/contrib/redirects/models.py
--- a/django/contrib/redirects/models.py
+++ b/django/contrib/redirects/models.py
@@ -1,6 +1,6 @@
 from django.db import models
 from django.contrib.sites.models import Site
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
 
 class Redirect(models.Model):
     site = models.ForeignKey(Site)
@@ -10,11 +10,13 @@
         help_text=_("This can be either an absolute path (as above) or a full URL starting with 'http://'."))
 
     class Meta:
-        verbose_name = _('redirect')
-        verbose_name_plural = _('redirects')
         db_table = 'django_redirect'
         unique_together=(('site', 'old_path'),)
         ordering = ('old_path',)
-    
+
     def __unicode__(self):
         return "%s ---> %s" % (self.old_path, self.new_path)
+
+    @classmethod
+    def verbose_names(cls, count=1):
+        return ungettext_lazy('redirect', 'redirects', count)
diff --git a/django/contrib/sessions/models.py b/django/contrib/sessions/models.py
--- a/django/contrib/sessions/models.py
+++ b/django/contrib/sessions/models.py
@@ -1,5 +1,5 @@
 from django.db import models
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
 
 
 class SessionManager(models.Manager):
@@ -43,12 +43,14 @@
 
     class Meta:
         db_table = 'django_session'
-        verbose_name = _('session')
-        verbose_name_plural = _('sessions')
 
     def get_decoded(self):
         return SessionStore().decode(self.session_data)
 
+    @classmethod
+    def verbose_names(cls, count=1):
+        return ungettext_lazy('session', 'sessions', count)
+
 
 # At bottom to avoid circular import
 from django.contrib.sessions.backends.db import SessionStore
diff --git a/django/contrib/sites/models.py b/django/contrib/sites/models.py
--- a/django/contrib/sites/models.py
+++ b/django/contrib/sites/models.py
@@ -1,5 +1,5 @@
 from django.db import models
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
 
 
 SITE_CACHE = {}
@@ -40,8 +40,6 @@
 
     class Meta:
         db_table = 'django_site'
-        verbose_name = _('site')
-        verbose_name_plural = _('sites')
         ordering = ('domain',)
 
     def __unicode__(self):
@@ -61,6 +59,10 @@
         except KeyError:
             pass
 
+    @classmethod
+    def verbose_names(cls, count=1):
+        return ungettext_lazy('site', 'sites', count)
+
 
 class RequestSite(object):
     """
diff --git a/django/db/models/base.py b/django/db/models/base.py
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -98,6 +98,10 @@
         for obj_name, obj in attrs.items():
             new_class.add_to_class(obj_name, obj)
 
+        if hasattr(new_class, 'verbose_names'):
+            new_class._meta._verbose_name = new_class._meta.get_verbose_name()
+            new_class._meta._verbose_name_plural = new_class._meta.get_verbose_name(0)
+
         # All the fields of any type declared on this model
         new_fields = new_class._meta.local_fields + \
                      new_class._meta.local_many_to_many + \
@@ -770,7 +774,7 @@
 
     def unique_error_message(self, model_class, unique_check):
         opts = model_class._meta
-        model_name = capfirst(opts.verbose_name)
+        model_name = capfirst(opts.get_verbose_name())
 
         # A unique field
         if len(unique_check) == 1:
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -115,7 +115,7 @@
     def set_attributes_from_rel(self):
         self.name = self.name or (self.rel.to._meta.object_name.lower() + '_' + self.rel.to._meta.pk.name)
         if self.verbose_name is None:
-            self.verbose_name = self.rel.to._meta.verbose_name
+            self.verbose_name = self.rel.to._meta.get_verbose_name()
         self.rel.field_name = self.rel.field_name or self.rel.to._meta.pk.name
 
     def do_related_class(self, other, cls):
@@ -940,7 +940,7 @@
         qs = qs.complex_filter(self.rel.limit_choices_to)
         if not qs.exists():
             raise exceptions.ValidationError(self.error_messages['invalid'] % {
-                'model': self.rel.to._meta.verbose_name, 'pk': value})
+                'model': self.rel.to._meta.get_verbose_name(), 'pk': value})
 
     def get_attname(self):
         return '%s_id' % self.name
@@ -1052,6 +1052,12 @@
 def create_many_to_many_intermediary_model(field, klass):
     from django.db import models
     managed = True
+
+    def verbose_name_method(f, t):
+        def verbose_names(cls, count=1):
+            return '%(from)s-%(to)s relationship%(extra)s' % {'from': f, 'to': t, 'extra': 's' if count != 1 else ''}
+        return verbose_names
+
     if isinstance(field.rel.to, basestring) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT:
         to_model = field.rel.to
         to = to_model.split('.')[-1]
@@ -1080,15 +1086,14 @@
         'app_label': klass._meta.app_label,
         'db_tablespace': klass._meta.db_tablespace,
         'unique_together': (from_, to),
-        'verbose_name': '%(from)s-%(to)s relationship' % {'from': from_, 'to': to},
-        'verbose_name_plural': '%(from)s-%(to)s relationships' % {'from': from_, 'to': to},
     })
     # Construct and return the new class.
     return type(name, (models.Model,), {
         'Meta': meta,
         '__module__': klass.__module__,
         from_: models.ForeignKey(klass, related_name='%s+' % name, db_tablespace=field.db_tablespace),
-        to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace)
+        to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace),
+        'verbose_names': classmethod(verbose_name_method(from_, to)),
     })
 
 class ManyToManyField(RelatedField, Field):
diff --git a/django/db/models/options.py b/django/db/models/options.py
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -1,30 +1,34 @@
+from bisect import bisect
 import re
-from bisect import bisect
+import warnings
 
 from django.conf import settings
-from django.db.models.related import RelatedObject
 from django.db.models.fields.related import ManyToManyRel
 from django.db.models.fields import AutoField, FieldDoesNotExist
 from django.db.models.fields.proxy import OrderWrt
 from django.db.models.loading import get_models, app_cache_ready
+from django.db.models.related import RelatedObject
+from django.utils.datastructures import SortedDict
+from django.utils.encoding import force_unicode, smart_str
+from django.utils.functional import cached_property
 from django.utils.translation import activate, deactivate_all, get_language, string_concat
-from django.utils.encoding import force_unicode, smart_str
-from django.utils.datastructures import SortedDict
 
-# Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces".
-get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip()
-
-DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
+DEFAULT_NAMES = ('db_table', 'ordering',
                  'unique_together', 'permissions', 'get_latest_by',
                  'order_with_respect_to', 'app_label', 'db_tablespace',
                  'abstract', 'managed', 'proxy', 'auto_created')
 
+DEPRECATED_NAMES = ('verbose_name', 'verbose_name_plural')
+
+CAMEL_CASE_RE = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
+
 class Options(object):
     def __init__(self, meta, app_label=None):
         self.local_fields, self.local_many_to_many = [], []
         self.virtual_fields = []
-        self.module_name, self.verbose_name = None, None
-        self.verbose_name_plural = None
+        self.cls = None
+        self.module_name, self._verbose_name = None, None
+        self._verbose_name_plural = None
         self.db_table = ''
         self.ordering = []
         self.unique_together =  []
@@ -54,16 +58,34 @@
         # from *other* models. Needed for some admin checks. Internal use only.
         self.related_fkey_lookups = []
 
+    def _get_verbose_name(self):
+        warnings.warn("Meta.verbose_name is deprecated. Use a verbose_names()"
+            " classmethod in the model instead.", PendingDeprecationWarning)
+        return self._verbose_name
+    def _set_verbose_name(self, value):
+        self._verbose_name = value
+    verbose_name = property(_get_verbose_name, _set_verbose_name)
+
+    def _get_verbose_name_plural(self):
+        warnings.warn("Meta.verbose_name_plural is deprecated. Use a "
+            "verbose_names() classmethod in the model instead.",
+            PendingDeprecationWarning)
+        return self._verbose_name_plural
+    def _set_verbose_name_plural(self, value):
+        self._verbose_name_plural = value
+    verbose_name_plural = property(_get_verbose_name_plural, _set_verbose_name_plural)
+
     def contribute_to_class(self, cls, name):
         from django.db import connection
         from django.db.backends.util import truncate_name
 
+        self.cls = cls
         cls._meta = self
         self.installed = re.sub('\.models$', '', cls.__module__) in settings.INSTALLED_APPS
         # First, construct the default values for these options.
         self.object_name = cls.__name__
         self.module_name = self.object_name.lower()
-        self.verbose_name = get_verbose_name(self.object_name)
+        self._verbose_name = CAMEL_CASE_RE.sub(' \\1', self.object_name).lower().strip()
 
         # Next, apply any overridden values from 'class Meta'.
         if self.meta:
@@ -80,6 +102,14 @@
                 elif hasattr(self.meta, attr_name):
                     setattr(self, attr_name, getattr(self.meta, attr_name))
 
+            for attr_name in DEPRECATED_NAMES:
+                if attr_name in meta_attrs:
+                    warnings.warn("%(cls)s: Meta.%(attr_name)s is deprecated. Use a "
+                        "verbose_names() classmethod in the model "
+                        "instead." % {'cls': cls, 'attr_name': attr_name},
+                        PendingDeprecationWarning)
+                    setattr(self, '_%s' % attr_name, meta_attrs.pop(attr_name))
+
             # unique_together can be either a tuple of tuples, or a single
             # tuple of two strings. Normalize it to a tuple of tuples, so that
             # calling code can uniformly expect that.
@@ -90,14 +120,14 @@
 
             # verbose_name_plural is a special case because it uses a 's'
             # by default.
-            if self.verbose_name_plural is None:
-                self.verbose_name_plural = string_concat(self.verbose_name, 's')
+            if self._verbose_name_plural is None:
+                self._verbose_name_plural = string_concat(self._verbose_name, 's')
 
             # Any leftover attributes must be invalid.
             if meta_attrs != {}:
                 raise TypeError("'class Meta' got invalid attribute(s): %s" % ','.join(meta_attrs.keys()))
         else:
-            self.verbose_name_plural = string_concat(self.verbose_name, 's')
+            self._verbose_name_plural = string_concat(self._verbose_name, 's')
         del self.meta
 
         # If the db_table wasn't provided, use the app_label + module_name.
@@ -199,10 +229,10 @@
         """
         lang = get_language()
         deactivate_all()
-        raw = force_unicode(self.verbose_name)
+        raw = force_unicode(self._verbose_name)
         activate(lang)
         return raw
-    verbose_name_raw = property(verbose_name_raw)
+    verbose_name_raw = cached_property(verbose_name_raw)
 
     def _fields(self):
         """
@@ -495,3 +525,12 @@
         Returns the index of the primary key field in the self.fields list.
         """
         return self.fields.index(self.pk)
+
+    def get_verbose_name(self, count=1):
+        if hasattr(self.cls, 'verbose_names'):
+            retv = self.cls.verbose_names(count)
+            if retv is not None:
+                return retv
+        if count == 1:
+            return CAMEL_CASE_RE.sub(' \\1', self.object_name).lower().strip()
+        return string_concat(self.get_verbose_name(1), 's')
diff --git a/django/db/models/related.py b/django/db/models/related.py
--- a/django/db/models/related.py
+++ b/django/db/models/related.py
@@ -36,7 +36,7 @@
                 {'%s__isnull' % self.parent_model._meta.module_name: False})
         lst = [(x._get_pk_val(), smart_unicode(x)) for x in queryset]
         return first_choice + lst
-        
+
     def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
         # Defer to the actual field definition for db prep
         return self.field.get_db_prep_lookup(lookup_type, value,
@@ -67,3 +67,6 @@
 
     def get_cache_name(self):
         return "_%s_cache" % self.get_accessor_name()
+
+    def get_verbose_name(self, count=1):
+        return self.model._meta.get_verbose_name(count)
diff --git a/django/utils/functional.py b/django/utils/functional.py
--- a/django/utils/functional.py
+++ b/django/utils/functional.py
@@ -31,8 +31,8 @@
 
 class cached_property(object):
     """
-    Decorator that creates converts a method with a single
-    self argument into a property cached on the instance.
+    Decorator that converts a method with a single self argument into a
+    property cached on the instance.
     """
     def __init__(self, func):
         self.func = func
diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py
--- a/django/views/generic/create_update.py
+++ b/django/views/generic/create_update.py
@@ -95,7 +95,7 @@
         return model.objects.get(**lookup_kwargs)
     except ObjectDoesNotExist:
         raise Http404("No %s found for %s"
-                      % (model._meta.verbose_name, lookup_kwargs))
+                      % (model._meta.get_verbose_name(), lookup_kwargs))
 
 def create_object(request, model=None, template_name=None,
         template_loader=loader, extra_context=None, post_save_redirect=None,
@@ -119,7 +119,7 @@
             new_object = form.save()
 
             msg = ugettext("The %(verbose_name)s was created successfully.") %\
-                                    {"verbose_name": model._meta.verbose_name}
+                                    {"verbose_name": model._meta.get_verbose_name()}
             messages.success(request, msg, fail_silently=True)
             return redirect(post_save_redirect, new_object)
     else:
@@ -162,7 +162,7 @@
         if form.is_valid():
             obj = form.save()
             msg = ugettext("The %(verbose_name)s was updated successfully.") %\
-                                    {"verbose_name": model._meta.verbose_name}
+                                    {"verbose_name": model._meta.get_verbose_name()}
             messages.success(request, msg, fail_silently=True)
             return redirect(post_save_redirect, obj)
     else:
@@ -205,7 +205,7 @@
     if request.method == 'POST':
         obj.delete()
         msg = ugettext("The %(verbose_name)s was deleted.") %\
-                                    {"verbose_name": model._meta.verbose_name}
+                                    {"verbose_name": model._meta.get_verbose_name()}
         messages.success(request, msg, fail_silently=True)
         return HttpResponseRedirect(post_delete_redirect)
     else:
diff --git a/django/views/generic/date_based.py b/django/views/generic/date_based.py
--- a/django/views/generic/date_based.py
+++ b/django/views/generic/date_based.py
@@ -34,7 +34,7 @@
         queryset = queryset.filter(**{'%s__lte' % date_field: datetime.datetime.now()})
     date_list = queryset.dates(date_field, 'year')[::-1]
     if not date_list and not allow_empty:
-        raise Http404("No %s available" % model._meta.verbose_name)
+        raise Http404("No %s available" % model._meta.get_verbose_name())
 
     if date_list and num_latest:
         latest = queryset.order_by('-'+date_field)[:num_latest]
@@ -354,7 +354,7 @@
     try:
         obj = queryset.get(**lookup_kwargs)
     except ObjectDoesNotExist:
-        raise Http404("No %s found for" % model._meta.verbose_name)
+        raise Http404("No %s found for" % model._meta.get_verbose_name())
     if not template_name:
         template_name = "%s/%s_detail.html" % (model._meta.app_label, model._meta.object_name.lower())
     if template_name_field:
diff --git a/django/views/generic/dates.py b/django/views/generic/dates.py
--- a/django/views/generic/dates.py
+++ b/django/views/generic/dates.py
@@ -195,7 +195,7 @@
 
         if not allow_empty and not qs:
             raise Http404(_(u"No %(verbose_name_plural)s available") % {
-                    'verbose_name_plural': force_unicode(qs.model._meta.verbose_name_plural)
+                    'verbose_name_plural': force_unicode(qs.model._meta.get_verbose_name(0))
             })
 
         return qs
@@ -464,7 +464,7 @@
 
         if not self.get_allow_future() and date > datetime.date.today():
             raise Http404(_(u"Future %(verbose_name_plural)s not available because %(class_name)s.allow_future is False.") % {
-                'verbose_name_plural': qs.model._meta.verbose_name_plural,
+                'verbose_name_plural': qs.model._meta.get_verbose_name(0),
                 'class_name': self.__class__.__name__,
             })
 
diff --git a/django/views/generic/detail.py b/django/views/generic/detail.py
--- a/django/views/generic/detail.py
+++ b/django/views/generic/detail.py
@@ -49,7 +49,7 @@
             obj = queryset.get()
         except ObjectDoesNotExist:
             raise Http404(_(u"No %(verbose_name)s found matching the query") %
-                          {'verbose_name': queryset.model._meta.verbose_name})
+                          {'verbose_name': queryset.model._meta.get_verbose_name()})
         return obj
 
     def get_queryset(self):
diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py
--- a/django/views/generic/list_detail.py
+++ b/django/views/generic/list_detail.py
@@ -131,7 +131,7 @@
     try:
         obj = queryset.get()
     except ObjectDoesNotExist:
-        raise Http404("No %s found matching the query" % (model._meta.verbose_name))
+        raise Http404("No %s found matching the query" % (model._meta.get_verbose_name()))
     if not template_name:
         template_name = "%s/%s_detail.html" % (model._meta.app_label, model._meta.object_name.lower())
     if template_name_field:
diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt
--- a/docs/ref/models/instances.txt
+++ b/docs/ref/models/instances.txt
@@ -540,12 +540,106 @@
 More details on named URL patterns are in the :doc:`URL dispatch documentation
 </topics/http/urls>`.
 
+``verbose_names``
+-----------------
+
+.. classmethod:: Model.verbose_names(count=1)
+
+.. versionadded:: 1.4
+
+A Python classmethod.
+
+This method has no default implementation and you might provide one depending
+on the the human readable name you need for your model.
+
+The *count* argument is the quantity of model instances the verbose name is
+being requested for.
+
+It provides a straight migration path from the :attr:`~Options.verbose_name` and
+:attr:`~Options.verbose_name_plural` options that entered a deprecation cycle
+starting with Django 1.4.  For example this model declaration::
+
+    class SSN(models.Model):
+        value = models.CharField(max_length=11)
+
+        class Meta:
+            verbose_name = 'social security number'
+
+needs to be changed to::
+
+    class SSN(models.Model):
+        value = models.CharField(max_length=11)
+
+        @classmethod
+        def verbose_names(cls, count=1):
+            return 'social security number'
+
+and this one::
+
+    class SecurityPolicy(models.Model):
+        title = models.CharField(max_length=30)
+
+        class Meta:
+            verbose_name_plural = 'security policies'
+
+should be changed to::
+
+    class SecurityPolicy(models.Model):
+        title = models.CharField(max_length=30)
+
+        @classmethod
+        def verbose_names(cls, count=1):
+            if count != 1:
+                return 'security policies'
+
+This new syntax can take in account the number of model instances at play to
+decide the exact verbose name to show in user interaction contexts. It provides
+for better internationalization of your application because the name of your
+model is now translatable in a more correct way to many more locales::
+
+    from django.utils.translation import ugettext_lazy
+
+    class Man(models.Model):
+        first_name = models.CharField(max_length=30)
+
+        @classmethod
+        def verbose_names(cls, count=1):
+            if count == 1:
+                return ugettext_lazy('man')
+            else:
+                return ugettext_lazy('men')
+
+Although you usually will use the
+:func:`~django.utils.translation.ungettext_lazy` function::
+
+    from django.utils.translation import ungettext_lazy
+
+    class Library(models.Model):
+        city_name = models.CharField(max_length=30)
+
+        @classmethod
+        def verbose_names(cls, count=1):
+            return ungetttext_lazy('llbrary', 'libraries', count)
+
+.. note::
+    Remember to declare this method as a classmethod::
+
+        class MyModel(models.Model):
+            ...
+
+            @classmethod
+            def verbose_names(cls, count=1):
+                ...
+
 Extra instance methods
 ======================
 
 In addition to :meth:`~Model.save()`, :meth:`~Model.delete()`, a model object
 might have some of the following methods:
 
+``get_*_display``
+-----------------
+
 .. method:: Model.get_FOO_display()
 
 For every field that has :attr:`~django.db.models.Field.choices` set, the
@@ -570,6 +664,9 @@
     >>> p.get_gender_display()
     'Male'
 
+``get_next_by_*`` and ``get_prev_by_*``
+---------------------------------------
+
 .. method:: Model.get_next_by_FOO(\**kwargs)
 .. method:: Model.get_previous_by_FOO(\**kwargs)
 
@@ -586,4 +683,3 @@
 Note that in the case of identical date values, these methods will use the
 primary key as a tie-breaker. This guarantees that no records are skipped or
 duplicated. That also means you cannot use those methods on unsaved objects.
-
diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt
--- a/docs/ref/models/options.txt
+++ b/docs/ref/models/options.txt
@@ -253,6 +253,11 @@
 
 .. attribute:: Options.verbose_name
 
+    .. deprecated:: 1.4
+        This option has been replaced by the :meth:`~Model.verbose_names` model
+        classmethod. Implement such method in your model to make Django aware
+        of its human readable name(s).
+
     A human-readable name for the object, singular::
 
         verbose_name = "pizza"
@@ -265,8 +270,72 @@
 
 .. attribute:: Options.verbose_name_plural
 
+    .. deprecated:: 1.4
+        This option has been replaced by the :meth:`~Model.verbose_names` model
+        classmethod. Implement such method in your model to make Django aware
+        of its human readable name(s).
+
     The plural name for the object::
 
         verbose_name_plural = "stories"
 
     If this isn't given, Django will use :attr:`~Options.verbose_name` + ``"s"``.
+
+``Meta`` methods
+================
+
+``get_verbose_name``
+--------------------
+
+.. method:: Options.get_verbose_name(count=1)
+
+.. versionadded:: 1.4
+
+It provides an API to access translated and correctly pluralized verbose names
+of models (something that previously involved accessing the
+``Model._meta.verbose_name`` and ``Model._meta.verbose_name_plural``
+attributes.)
+
+.. seealso::
+
+    The :meth:`~Model.verbose_names` user-provided classmethod that works
+    together with this method.
+
+This method will always return a value independently of whether the model
+implements the :meth:`~Model.verbose_names` classmethod or not. Django provides
+fallback return values compatible with the default values of the deprecated
+:attr:`~Options.verbose_name` and :attr:`~Options.verbose_name_plural` options.
+
+For example, given this model::
+
+    class Door(models.Model):
+        height = models.PositiveIntegerField()
+
+then these are the return values of this method::
+
+    >>> Door._meta.get_verbose_name(1) # One door
+    >>> 'door' # Automatically provided singular verbose name
+    >>> Door._meta.get_verbose_name(3) # More than one door
+    >>> 'doors'
+    # Note how it returns an automatically provided simple naive pluralization
+    # appending a 's' to the singular value
+
+Or, for the examples in the :meth:`~Model.verbose_names` documentation::
+
+    >>> SSN._meta.get_verbose_name() # One SSN, count default value
+    >>> 'social security number'
+    # Note how it returns the value returned by SSN.verbose_names(count) for a
+    # value of count=1
+    >>> SSN._meta.get_verbose_name(0) # Zero SSN
+    >>> 'social security numbers'
+    # Note how it returns an automatically provided simple naive pluralization
+    # appending a 's' to the singular value
+
+    >>> SecurityPolicy._meta.get_verbose_name() # One policy
+    >>> 'security policy'
+    # Note how it returns a value automatically provided by Django by processing
+    # the model class name
+    >>> SecurityPolicy._meta.get_verbose_name(10) # Ten policies
+    >>> 'security policies'
+    # Note how it returns the value returned by
+    # SecurityPolicy.verbose_names(count) for a count value different from 1
diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt
--- a/docs/topics/db/models.txt
+++ b/docs/topics/db/models.txt
@@ -641,13 +641,10 @@
 
         class Meta:
             ordering = ["horn_length"]
-            verbose_name_plural = "oxen"
 
 Model metadata is "anything that's not a field", such as ordering options
-(:attr:`~Options.ordering`), database table name (:attr:`~Options.db_table`), or
-human-readable singular and plural names (:attr:`~Options.verbose_name` and
-:attr:`~Options.verbose_name_plural`). None are required, and adding ``class
-Meta`` to a model is completely optional.
+(:attr:`~Options.ordering`), database table name (:attr:`~Options.db_table`).
+None are required, and adding ``class Meta`` to a model is completely optional.
 
 A complete list of all possible ``Meta`` options can be found in the :doc:`model
 option reference </ref/models/options>`.
diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt
--- a/docs/topics/i18n/translation.txt
+++ b/docs/topics/i18n/translation.txt
@@ -168,11 +168,10 @@
 translation string and the number of objects.
 
 This function is useful when you need your Django application to be localizable
-to languages where the number and complexity of `plural forms
-<http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms>`_ is
-greater than the two forms used in English ('object' for the singular and
-'objects' for all the cases where ``count`` is different from one, irrespective
-of its value.)
+to languages where the number and complexity of `plural forms`_ is greater than
+the two forms as used in the English language (e.g. 'object' for the singular
+and 'objects' for all the cases where ``count`` is different from one,
+irrespective of its value.)
 
 For example::
 
@@ -187,18 +186,15 @@
         }
         return HttpResponse(page)
 
-In this example the number of objects is passed to the translation
-languages as the ``count`` variable.
+In this example the number of objects is passed to the translation functions as
+the ``count`` variable.
 
-Lets see a slightly more complex usage example::
+Lets see a slightly more complex example::
 
     from django.utils.translation import ungettext
 
     count = Report.objects.count()
-    if count == 1:
-        name = Report._meta.verbose_name
-    else:
-        name = Report._meta.verbose_name_plural
+    name = Report._meta.get_verbose_name(count)
 
     text = ungettext(
             'There is %(count)d %(name)s available.',
@@ -209,10 +205,12 @@
         'name': name
     }
 
-Here we reuse localizable, hopefully already translated literals (contained in
-the ``verbose_name`` and ``verbose_name_plural`` model ``Meta`` options) for
-other parts of the sentence so all of it is consistently based on the
-cardinality of the elements at play.
+Here we reuse localizable, potentially translated literals (as returned by the
+:meth:`django.db.models.Options.get_verbose_name` method) for other parts of the
+sentence so all of it is consistently based on the cardinality of the elements
+at play.
+
+.. _plural forms: http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms
 
 .. _pluralization-var-notes:
 
@@ -294,11 +292,24 @@
 Lazy translation
 ----------------
 
-Use the function :func:`django.utils.translation.ugettext_lazy()` to translate
-strings lazily -- when the value is accessed rather than when the
-``ugettext_lazy()`` function is called.
+Use the lazy versions of translation functions in
+:mod:`django.utils.translation` (easily recognizable by the ``lazy`` suffix in
+their names) to translate strings lazily -- when the value is accessed rather
+than when they are called.
 
-For example, to translate a model's ``help_text``, do the following::
+This is an essential need when we need to mark for translation text that is
+intermixed with code that is executed at module load time.
+
+As this is something that can easily happen when defining Django models (the
+declarative notation is implemented in a way such that model fields are actually
+class level attributes) this means you need to make sure to use lazy
+translations in the following cases:
+
+Model fields and relationship ``verbose_name`` and ``help_text`` option values
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For example, to translate the help text of the *name* field in the following
+model, do the following::
 
     from django.utils.translation import ugettext_lazy
 
@@ -310,6 +321,65 @@
 is used in a string context, such as template rendering on the Django admin
 site.
 
+In addition to normal fields your models might also contain relationships:
+``ForeignKey``, ``ManyTomanyField`` or ``OneToOneField`` fields. You can mark
+the name of a relationship itself as translatable by using its ``verbose_name``
+option::
+
+    from django.utils.translation import ugettext_lazy as _
+
+    class MyThing(models.Model):
+        kind = models.ForeignKey(ThingKind, related_name='kinds',
+                                 verbose_name=_('kind'))
+
+Just like you would do in the :meth:`~django.db.models.Model.verbose_names`
+model classmethod, you should provide a lowercase verbose name text for the
+relation as Django will automatically titlecase it when required.
+
+Model verbose names values
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It is recommended to always provide a
+:meth:`~django.db.models.Model.verbose_names` method rather than relying on
+Django's default English-centric and somewhat naïve determination of verbose
+names::
+
+    from django.utils.translation import ugettext_lazy
+
+    class MyThing(models.Model):
+        name = models.CharField(_('name'), help_text=ugettext_lazy('This is the help text'))
+
+        @classmethod
+        def verbose_names(cls, count=1):
+            if count == 1:
+                return ugettext_lazy('my thing')
+            else:
+                return ugettext_lazy('my things')
+
+In this particular case it is almost always a better idea to actually use
+:func:`~django.utils.translation.ungettext_lazy` instead of ``ugettext_lazy``.
+Refer to the ``verbose_names()`` documentation linked above for details on how
+this can make the lifes of your translators and users easier.
+
+Model methods ``short_description`` attribute values
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For model methods, you can provide translations to Django and the admin site
+with the ``short_description`` parameter set on the corresponding method::
+
+    from django.utils.translation import ugettext_lazy as _
+
+    class MyThing(models.Model):
+        kind = models.ForeignKey(ThingKind, related_name='kinds',
+                                 verbose_name=_('kind'))
+
+        def is_mouse(self):
+            return self.kind.type == MOUSE_TYPE
+        is_mouse.short_description = _('Is it a mouse?')
+
+Notes on translation in models
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 The result of a ``ugettext_lazy()`` call can be used wherever you would use a
 unicode string (an object with type ``unicode``) in Python. If you try to use
 it where a bytestring (a ``str`` object) is expected, things will not work as
@@ -328,7 +398,7 @@
 <django.utils.functional...>"``, you have tried to insert the result of
 ``ugettext_lazy()`` into a bytestring. That's a bug in your code.
 
-If you don't like the verbose name ``ugettext_lazy``, you can just alias it as
+If you don't like the long ``ugettext_lazy`` name, you can just alias it as
 ``_`` (underscore), like so::
 
     from django.utils.translation import ugettext_lazy as _
@@ -336,56 +406,6 @@
     class MyThing(models.Model):
         name = models.CharField(help_text=_('This is the help text'))
 
-Always use lazy translations in :doc:`Django models </topics/db/models>`.
-Field names and table names should be marked for translation (otherwise, they
-won't be translated in the admin interface). This means writing explicit
-``verbose_name`` and ``verbose_name_plural`` options in the ``Meta`` class,
-though, rather than relying on Django's default determination of
-``verbose_name`` and ``verbose_name_plural`` by looking at the model's class
-name::
-
-    from django.utils.translation import ugettext_lazy as _
-
-    class MyThing(models.Model):
-        name = models.CharField(_('name'), help_text=_('This is the help text'))
-
-        class Meta:
-            verbose_name = _('my thing')
-            verbose_name_plural = _('my things')
-
-Notes on model classes translation
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Your model classes may not only contain normal fields: you may have relations
-(with a ``ForeignKey`` field) or additional model methods you may use for
-columns in the Django admin site.
-
-If you have models with foreign keys and you use the Django admin site, you can
-provide translations for the relation itself by using the ``verbose_name``
-parameter on the ``ForeignKey`` object::
-
-    class MyThing(models.Model):
-        kind = models.ForeignKey(ThingKind, related_name='kinds',
-                                 verbose_name=_('kind'))
-
-As you would do for the ``verbose_name`` and ``verbose_name_plural`` settings of
-a model Meta class, you should provide a lowercase verbose name text for the
-relation as Django will automatically titlecase it when required.
-
-For model methods, you can provide translations to Django and the admin site
-with the ``short_description`` parameter set on the corresponding method::
-
-    class MyThing(models.Model):
-        kind = models.ForeignKey(ThingKind, related_name='kinds',
-                                 verbose_name=_('kind'))
-
-        def is_mouse(self):
-            return self.kind.type == MOUSE_TYPE
-        is_mouse.short_description = _('Is it a mouse?')
-
-As always with model classes translations, don't forget to use the lazy
-translation method!
-
 Working with lazy translation objects
 -------------------------------------
 
diff --git a/tests/modeltests/custom_pk/models.py b/tests/modeltests/custom_pk/models.py
--- a/tests/modeltests/custom_pk/models.py
+++ b/tests/modeltests/custom_pk/models.py
@@ -26,12 +26,14 @@
 class Business(models.Model):
     name = models.CharField(max_length=20, primary_key=True)
     employees = models.ManyToManyField(Employee)
-    class Meta:
-        verbose_name_plural = 'businesses'
 
     def __unicode__(self):
         return self.name
 
+    @classmethod
+    def verbose_names(cls, count=1):
+        return 'businesses'
+
 class Bar(models.Model):
     id = MyAutoField(primary_key=True, db_index=True)
 
diff --git a/tests/regressiontests/admin_util/models.py b/tests/regressiontests/admin_util/models.py
--- a/tests/regressiontests/admin_util/models.py
+++ b/tests/regressiontests/admin_util/models.py
@@ -34,5 +34,6 @@
     event = models.OneToOneField(Event)
     name = models.CharField(max_length=255)
 
-    class Meta:
-        verbose_name = "awesome guest"
+    @classmethod
+    def verbose_names(cls, count=1):
+        return "awesome guest"
diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
--- a/tests/regressiontests/admin_views/models.py
+++ b/tests/regressiontests/admin_views/models.py
@@ -63,9 +63,10 @@
     def __unicode__(self):
         return self.title
 
-    class Meta:
+    @classmethod
+    def verbose_names(cls, count=1):
         # Use a utf-8 bytestring to ensure it works (see #11710)
-        verbose_name = '¿Chapter?'
+        return '¿Chapter?'
 
 
 class ChapterXtra1(models.Model):
@@ -538,13 +539,13 @@
     age = models.PositiveIntegerField()
     is_employee = models.NullBooleanField()
 
-class PrePopulatedPostLargeSlug(models.Model): 
-    """ 
-    Regression test for #15938: a large max_length for the slugfield must not 
-    be localized in prepopulated_fields_js.html or it might end up breaking 
-    the javascript (ie, using THOUSAND_SEPARATOR ends up with maxLength=1,000) 
-    """ 
-    title = models.CharField(max_length=100) 
-    published = models.BooleanField() 
+class PrePopulatedPostLargeSlug(models.Model):
+    """
+    Regression test for #15938: a large max_length for the slugfield must not
+    be localized in prepopulated_fields_js.html or it might end up breaking
+    the javascript (ie, using THOUSAND_SEPARATOR ends up with maxLength=1,000)
+    """
+    title = models.CharField(max_length=100)
+    published = models.BooleanField()
     slug = models.SlugField(max_length=1000)
-    
+
diff --git a/tests/regressiontests/backends/models.py b/tests/regressiontests/backends/models.py
--- a/tests/regressiontests/backends/models.py
+++ b/tests/regressiontests/backends/models.py
@@ -28,14 +28,16 @@
 # Until #13711 is fixed, this test can't be run under MySQL.
 if connection.features.supports_long_model_names:
     class VeryLongModelNameZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ(models.Model):
-        class Meta:
-            # We need to use a short actual table name or
-            # we hit issue #8548 which we're not testing!
-            verbose_name = 'model_with_long_table_name'
         primary_key_is_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.AutoField(primary_key=True)
         charfield_is_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.CharField(max_length=100)
         m2m_also_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.ManyToManyField(Person,blank=True)
 
+        # We need to use a short actual verbose name or
+        # we hit issue #8548 which we're not testing!
+        @classmethod
+        def verbose_names(cls, count=1):
+            return 'model_with_long_table_name'
+
 
 class Tag(models.Model):
     name = models.CharField(max_length=30)
diff --git a/tests/regressiontests/generic_views/models.py b/tests/regressiontests/generic_views/models.py
--- a/tests/regressiontests/generic_views/models.py
+++ b/tests/regressiontests/generic_views/models.py
@@ -1,13 +1,11 @@
 from django.db import models
-
+from django.utils.translation import ungettext_lazy
 
 class Artist(models.Model):
     name = models.CharField(max_length=100)
 
     class Meta:
         ordering = ['name']
-        verbose_name = 'professional artist'
-        verbose_name_plural = 'professional artists'
 
     def __unicode__(self):
         return self.name
@@ -16,6 +14,10 @@
     def get_absolute_url(self):
         return ('artist_detail', (), {'pk': self.id})
 
+    @classmethod
+    def verbose_names(cls, count=1):
+        return ungettext_lazy('professional artist', 'professional artists', count)
+
 class Author(models.Model):
     name = models.CharField(max_length=100)
     slug = models.SlugField()
diff --git a/tests/regressiontests/i18n/models.py b/tests/regressiontests/i18n/models.py
--- a/tests/regressiontests/i18n/models.py
+++ b/tests/regressiontests/i18n/models.py
@@ -13,5 +13,6 @@
     cents_payed = models.DecimalField(max_digits=4, decimal_places=2)
     products_delivered = models.IntegerField()
 
-    class Meta:
-        verbose_name = _('Company')
\ No newline at end of file
+    @classmethod
+    def verbose_names(cls, count=1):
+        return _('Company')
diff --git a/tests/regressiontests/model_inheritance_regress/models.py b/tests/regressiontests/model_inheritance_regress/models.py
--- a/tests/regressiontests/model_inheritance_regress/models.py
+++ b/tests/regressiontests/model_inheritance_regress/models.py
@@ -108,7 +108,10 @@
 
     class Meta:
         abstract = True
-        verbose_name_plural = u'Audits'
+
+    @classmethod
+    def verbose_names(cls, count=1):
+        return u'Audits'
 
 class CertificationAudit(AuditBase):
     class Meta(AuditBase.Meta):
diff --git a/tests/regressiontests/model_regress/models.py b/tests/regressiontests/model_regress/models.py
--- a/tests/regressiontests/model_regress/models.py
+++ b/tests/regressiontests/model_regress/models.py
@@ -16,12 +16,15 @@
 
     class Meta:
         ordering = ('pub_date','headline')
-        # A utf-8 verbose name (Ångström's Articles) to test they are valid.
-        verbose_name = "\xc3\x85ngstr\xc3\xb6m's Articles"
 
     def __unicode__(self):
         return self.headline
 
+    @classmethod
+    def verbose_names(cls, count=1):
+        # An utf-8 verbose name (Ångström's Articles) to test they are valid.
+        return "\xc3\x85ngstr\xc3\xb6m's Articles"
+
 class Movie(models.Model):
     #5218: Test models with non-default primary keys / AutoFields
     movie_id = models.AutoField(primary_key=True)
diff --git a/tests/regressiontests/verbose_names/__init__.py b/tests/regressiontests/verbose_names/__init__.py
new file mode 100644
diff --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
new file mode 100644
index 0000000000000000000000000000000000000000..5a0d607200b2b05b66bf95085b76d736061c03bf
GIT binary patch
diff --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
new file mode 100644
--- /dev/null
+++ b/tests/regressiontests/verbose_names/locale/es_AR/LC_MESSAGES/django.po
@@ -0,0 +1,41 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-11-27 12:11-0600\n"
+"PO-Revision-Date: 2011-11-27 15:00-0300\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+
+msgid "Translatable legacy model #1"
+msgstr "Modelo legado traducible #1"
+
+msgid "Translatable legacy model #2"
+msgstr "Modelo legado traducible #2"
+
+msgid "Translatable legacy models #2"
+msgstr "Modelos legados traducibles #2"
+
+msgid "Translatable legacy models #3"
+msgstr "Modelos legados traducibles #3"
+
+msgid "Translatable New-style model #1"
+msgstr "Modelo moderno traducible #1"
+
+msgid "Translatable New-style model #2"
+msgid_plural "Translatable New-style models #2"
+msgstr[0] "Modelo moderno traducible #2"
+msgstr[1] "Modelos modernos traducibles #2"
+
+msgid "Translatable New-style models #3"
+msgstr "Modelos modernos traducibles #3"
diff --git a/tests/regressiontests/verbose_names/models.py b/tests/regressiontests/verbose_names/models.py
new file mode 100644
--- /dev/null
+++ b/tests/regressiontests/verbose_names/models.py
@@ -0,0 +1,95 @@
+from django.db import models
+from django.utils.translation import ugettext_lazy as _, ungettext_lazy
+
+
+class NoMeta(models.Model):
+    name = models.CharField(max_length=20)
+
+class LegacyOnlyVerboseName(models.Model):
+    name = models.CharField(max_length=20)
+
+    class Meta:
+        verbose_name = 'Legacy model #1'
+
+class LegacyBothVNames(models.Model):
+    name = models.CharField(max_length=20)
+
+    class Meta:
+        verbose_name = 'Legacy model #2'
+        verbose_name_plural = 'Legacy models #2'
+
+class LegacyOnlyVerboseNamePlural(models.Model):
+    name = models.CharField(max_length=20)
+
+    class Meta:
+        verbose_name_plural = 'Legacy models #3'
+
+class LegacyOnlyVerbosenameIntl(models.Model):
+    name = models.CharField(max_length=20)
+
+    class Meta:
+        verbose_name = _('Translatable legacy model #1')
+
+class LegacyBothIntl(models.Model):
+    name = models.CharField(max_length=20)
+
+    class Meta:
+        verbose_name = _('Translatable legacy model #2')
+        verbose_name_plural = _('Translatable legacy models #2')
+
+class LegacyPluralIntl(models.Model):
+    name = models.CharField(max_length=20)
+
+    class Meta:
+        verbose_name_plural = _('Translatable legacy models #3')
+
+# === Models using new classmethod syntax for verbose names ==========
+
+class NewstyleSingular(models.Model):
+    name = models.CharField(max_length=20)
+
+    @classmethod
+    def verbose_names(cls, count=1):
+        if count == 1:
+            return 'New-style model #1'
+
+class NewstyleBoth(models.Model):
+    name = models.CharField(max_length=20)
+
+    @classmethod
+    def verbose_names(cls, count=1):
+        if count == 1:
+            return 'New-style model #2'
+        else:
+            return 'New-style models #2'
+
+class NewstylePlural(models.Model):
+    name = models.CharField(max_length=20)
+
+    @classmethod
+    def verbose_names(cls, count=1):
+        if count != 1:
+            return 'New-style models #3'
+
+class NewstyleSingularIntl(models.Model):
+    name = models.CharField(max_length=20)
+
+    @classmethod
+    def verbose_names(cls, count=1):
+        if count == 1:
+            return _('Translatable New-style model #1')
+
+class NewstyleBothIntl(models.Model):
+    name = models.CharField(max_length=20)
+
+    @classmethod
+    def verbose_names(cls, count=1):
+        return ungettext_lazy('Translatable New-style model #2', 'Translatable New-style models #2', count)
+
+class NewstylePluralIntl(models.Model):
+    name = models.CharField(max_length=20)
+
+    @classmethod
+    def verbose_names(cls, count=1):
+        if count != 1:
+            return _('Translatable New-style models #3')
diff --git a/tests/regressiontests/verbose_names/tests.py b/tests/regressiontests/verbose_names/tests.py
new file mode 100644
--- /dev/null
+++ b/tests/regressiontests/verbose_names/tests.py
@@ -0,0 +1,246 @@
+from __future__ import absolute_import
+
+from django.utils.encoding import force_unicode
+from django.utils import translation
+from django.utils.unittest import TestCase
+
+from .models import (NoMeta, LegacyOnlyVerboseName, LegacyBothVNames,
+        LegacyOnlyVerboseNamePlural, LegacyOnlyVerbosenameIntl, LegacyBothIntl,
+        LegacyPluralIntl, NewstyleSingular, NewstyleBoth, NewstylePlural,
+        NewstyleSingularIntl, NewstyleBothIntl, NewstylePluralIntl)
+
+
+class LegacyVerboseNameNoI18NTests(TestCase):
+    """
+    Test we don't disrupt behavior associated with legacy
+    Meta.verbose_name{,_plural} attributes when translation isn't used.
+    """
+
+    def test_noi18n_no_meta_inner_class(self):
+        # A model without an inner Meta class
+        a = NoMeta.objects.create(name=u'Name')
+        self.assertEqual('no meta', NoMeta._meta.verbose_name)
+        self.assertEqual('no meta', a._meta.verbose_name)
+        # Automatically generated plural form, can be bogus (note the arbitrary
+        # 's' tucked at the end)
+        self.assertEqual('no metas', force_unicode(NoMeta._meta.verbose_name_plural))
+        self.assertEqual('no metas', force_unicode(a._meta.verbose_name_plural))
+
+    def test_noi18n_only_verbose_name_option(self):
+        a = LegacyOnlyVerboseName.objects.create(name=u'Name')
+        # The verbose_name we specified
+        self.assertEqual('Legacy model #1', LegacyOnlyVerboseName._meta.verbose_name)
+        self.assertEqual('Legacy model #1', a._meta.verbose_name)
+        # Automatically generated plural form, can be bogus (note the arbitrary
+        # 's' tucked at the end)
+        self.assertEqual('Legacy model #1s', force_unicode(LegacyOnlyVerboseName._meta.verbose_name_plural))
+        self.assertEqual('Legacy model #1s', force_unicode(a._meta.verbose_name_plural))
+
+    def test_noi18n_both_verbose_name_options(self):
+        b = LegacyBothVNames.objects.create(name=u'Name')
+        # The verbose_name we specified
+        self.assertEqual('Legacy model #2', LegacyBothVNames._meta.verbose_name)
+        self.assertEqual('Legacy model #2', b._meta.verbose_name)
+        # The verbose_name_plural we specified
+        self.assertEqual('Legacy models #2', LegacyBothVNames._meta.verbose_name_plural)
+        self.assertEqual('Legacy models #2', b._meta.verbose_name_plural)
+
+    def test_noi18n_only_verbose_name_plural_option(self):
+        c = LegacyOnlyVerboseNamePlural.objects.create(name=u'Name')
+        # Verbose name automatically generated from the class name
+        self.assertEqual('legacy only verbose name plural', LegacyOnlyVerboseNamePlural._meta.verbose_name)
+        self.assertEqual('legacy only verbose name plural', c._meta.verbose_name)
+        # The verbose_name_plural we specified
+        self.assertEqual('Legacy models #3', LegacyOnlyVerboseNamePlural._meta.verbose_name_plural)
+        self.assertEqual('Legacy models #3', c._meta.verbose_name_plural)
+
+
+class LegacyVerboseNameI18NTests(TestCase):
+    """
+    Test we don't disrupt behavior associated with legacy
+    Meta.verbose_name{,_plural} attributes when translation is used.
+    """
+
+    def setUp(self):
+        translation.activate('es-ar')
+
+    def tearDown(self):
+        translation.deactivate()
+
+    def test_i18n_no_meta_inner_class(self):
+        # A model without an inner Meta class
+        a = NoMeta.objects.create(name=u'Name')
+        self.assertEqual('no meta', NoMeta._meta.verbose_name)
+        self.assertEqual('no meta', a._meta.verbose_name)
+        # Automatically generated plural form, can be bogus (note the arbitrary
+        # 's' tucked at the end)
+        self.assertEqual('no metas', force_unicode(NoMeta._meta.verbose_name_plural))
+        self.assertEqual('no metas', force_unicode(a._meta.verbose_name_plural))
+
+    def test_i18n_only_verbose_name_option(self):
+        a = LegacyOnlyVerbosenameIntl.objects.create(name=u'Name')
+        # The verbose_name we specified
+        self.assertEqual('Modelo legado traducible #1', force_unicode(LegacyOnlyVerbosenameIntl._meta.verbose_name))
+        self.assertEqual('Modelo legado traducible #1', a._meta.verbose_name)
+        # Automatically generated plural form, can be bogus (note the arbitrary
+        # 's' tucked at the end)
+        self.assertEqual('Modelo legado traducible #1s', force_unicode(LegacyOnlyVerbosenameIntl._meta.verbose_name_plural))
+        self.assertEqual('Modelo legado traducible #1s', force_unicode(a._meta.verbose_name_plural))
+
+    def test_i18n_both_verbose_name_options(self):
+        a = LegacyBothIntl.objects.create(name=u'Name')
+        # The verbose_name we specified
+        self.assertEqual('Modelo legado traducible #2', LegacyBothIntl._meta.verbose_name)
+        self.assertEqual('Modelo legado traducible #2', a._meta.verbose_name)
+        # The verbose_name_plural we specified
+        self.assertEqual('Modelos legados traducibles #2', LegacyBothIntl._meta.verbose_name_plural)
+        self.assertEqual('Modelos legados traducibles #2', a._meta.verbose_name_plural)
+
+    def test_i18n_only_verbose_name_plural_option(self):
+        a = LegacyPluralIntl.objects.create(name=u'Name')
+        # Verbose name automatically generated from the class name
+        self.assertEqual('legacy plural intl', LegacyPluralIntl._meta.verbose_name)
+        self.assertEqual('legacy plural intl', a._meta.verbose_name)
+        # The verbose_name_plural we specified
+        self.assertEqual('Modelos legados traducibles #3', LegacyPluralIntl._meta.verbose_name_plural)
+        self.assertEqual('Modelos legados traducibles #3', a._meta.verbose_name_plural)
+
+
+class VerboseNameNoI18NTests(TestCase):
+    """
+    Test new verbose_names() model classmethod behavior when translation isn't
+    used.
+    """
+
+    def test_backward_compatibility(self):
+        """
+        Test backward compatibility with legacy Meta.verbose_name{,_plural}
+        attributes.
+        """
+        a = NewstyleSingular.objects.create(name=u'Name')
+        # The verbose_name derived from the verbose_names() method we specified
+        self.assertEqual('New-style model #1', NewstyleSingular._meta.verbose_name)
+        self.assertEqual('New-style model #1', a._meta.verbose_name)
+        # Automatically generated plural form, can be bogus (note the arbitrary
+        # 's' tucked at the end)
+        self.assertEqual('New-style model #1s', force_unicode(NewstyleSingular._meta.verbose_name_plural))
+        self.assertEqual('New-style model #1s', force_unicode(a._meta.verbose_name_plural))
+
+        b = NewstyleBoth.objects.create(name=u'Name')
+        # The verbose_name derived from the verbose_names() we specified
+        self.assertEqual('New-style model #2', NewstyleBoth._meta.verbose_name)
+        self.assertEqual('New-style model #2', b._meta.verbose_name)
+        # The verbose_name_plural derived from the verbose_names() method we
+        # specified
+        self.assertEqual('New-style models #2', NewstyleBoth._meta.verbose_name_plural)
+        self.assertEqual('New-style models #2', b._meta.verbose_name_plural)
+
+        c = NewstylePlural.objects.create(name=u'Name')
+        # Verbose name automatically generated from the class name
+        self.assertEqual('newstyle plural', NewstylePlural._meta.verbose_name)
+        self.assertEqual('newstyle plural', c._meta.verbose_name)
+        # The verbose_name_plural derived from the verbose_names() method we
+        # specified
+        self.assertEqual('New-style models #3', NewstylePlural._meta.verbose_name_plural)
+        self.assertEqual('New-style models #3', c._meta.verbose_name_plural)
+
+    def test_new_behavior(self):
+        """
+        Test sanity of new verbose_names() model classmethod.
+        """
+        a = NewstyleSingular.objects.create(name=u'Name')
+        self.assertEqual('New-style model #1', NewstyleSingular._meta.get_verbose_name())
+        self.assertEqual('New-style model #1', a._meta.get_verbose_name())
+        # Fallback get_verbose_name() implementation, its return value
+        # can be bogus (note the arbitrary 's' tucked at the end)
+        self.assertEqual('New-style model #1s', force_unicode(NewstyleSingular._meta.get_verbose_name(0)))
+        self.assertEqual('New-style model #1s', force_unicode(a._meta.get_verbose_name(0)))
+
+        b = NewstyleBoth.objects.create(name=u'Name')
+        self.assertEqual('New-style model #2', NewstyleBoth._meta.get_verbose_name())
+        self.assertEqual('New-style model #2', b._meta.get_verbose_name())
+
+        self.assertEqual('New-style models #2', NewstyleBoth._meta.get_verbose_name(0))
+        self.assertEqual('New-style models #2', b._meta.get_verbose_name(0))
+
+        c = NewstylePlural.objects.create(name=u'Name')
+        # Fallback get_verbose_name() implementation: Returns a value
+        # automatically generated from the class name
+        self.assertEqual('newstyle plural', NewstylePlural._meta.get_verbose_name())
+        self.assertEqual('newstyle plural', c._meta.get_verbose_name())
+
+        self.assertEqual('New-style models #3', NewstylePlural._meta.get_verbose_name(0))
+        self.assertEqual('New-style models #3', c._meta.get_verbose_name(0))
+
+
+class VerboseNameI18NTests(TestCase):
+    """
+    Test new verbose_names() model classmethod behavior when translation is
+    used.
+    """
+
+    def setUp(self):
+        translation.activate('es-ar')
+
+    def tearDown(self):
+        translation.deactivate()
+
+    def test_backward_compatibility(self):
+        """
+        Test backward compatibility with legacy Meta.verbose_name{,_plural}
+        attributes.
+        """
+        a = NewstyleSingularIntl.objects.create(name=u'Name')
+        # The verbose_name derived from the verbose_names() method we specified
+        self.assertEqual('Modelo moderno traducible #1', NewstyleSingularIntl._meta.verbose_name)
+        self.assertEqual('Modelo moderno traducible #1', a._meta.verbose_name)
+        # Automatically generated plural form, can be bogus (note the arbitrary
+        # 's' tucked at the end)
+        self.assertEqual('Modelo moderno traducible #1s', force_unicode(NewstyleSingularIntl._meta.verbose_name_plural))
+        self.assertEqual('Modelo moderno traducible #1s', force_unicode(a._meta.verbose_name_plural))
+
+        b = NewstyleBothIntl.objects.create(name=u'Name')
+        # The verbose_name derived from the verbose_names() we specified
+        self.assertEqual('Modelo moderno traducible #2', force_unicode(NewstyleBothIntl._meta.verbose_name))
+        self.assertEqual('Modelo moderno traducible #2', b._meta.verbose_name)
+        # The verbose_name_plural derived from the verbose_names() method we
+        # specified
+        self.assertEqual('Modelos modernos traducibles #2', NewstyleBothIntl._meta.verbose_name_plural)
+        self.assertEqual('Modelos modernos traducibles #2', b._meta.verbose_name_plural)
+
+        c = NewstylePluralIntl.objects.create(name=u'Name')
+        # Verbose name automatically generated from the class name -- untranslatable
+        self.assertEqual('newstyle plural intl', NewstylePluralIntl._meta.verbose_name)
+        self.assertEqual('newstyle plural intl', c._meta.verbose_name)
+        # The verbose_name_plural derived from the verbose_names() method we
+        # specified
+        self.assertEqual('Modelos modernos traducibles #3', NewstylePluralIntl._meta.verbose_name_plural)
+        self.assertEqual('Modelos modernos traducibles #3', c._meta.verbose_name_plural)
+
+    def test_new_behavior(self):
+        """
+        Test sanity of new verbose_names() model classmethod.
+        """
+        a = NewstyleSingularIntl.objects.create(name=u'Name')
+        self.assertEqual('Modelo moderno traducible #1', NewstyleSingularIntl._meta.get_verbose_name())
+        self.assertEqual('Modelo moderno traducible #1', a._meta.get_verbose_name())
+        # Fallback get_verbose_name() implementation, its return value
+        # can be bogus (note the arbitrary 's' tucked at the end)
+        self.assertEqual('Modelo moderno traducible #1s', force_unicode(NewstyleSingularIntl._meta.get_verbose_name(0)))
+        self.assertEqual('Modelo moderno traducible #1s', force_unicode(a._meta.get_verbose_name(0)))
+
+        b = NewstyleBothIntl.objects.create(name=u'Name')
+        self.assertEqual('Modelo moderno traducible #2', NewstyleBothIntl._meta.get_verbose_name())
+        self.assertEqual('Modelo moderno traducible #2', b._meta.get_verbose_name())
+
+        self.assertEqual('Modelos modernos traducibles #2', NewstyleBothIntl._meta.get_verbose_name(0))
+        self.assertEqual('Modelos modernos traducibles #2', b._meta.get_verbose_name(0))
+
+        c = NewstylePluralIntl.objects.create(name=u'Name')
+        # Fallback get_verbose_name() implementation: Returns a value
+        # automatically generated from the class name -- untranslatable
+        self.assertEqual('newstyle plural intl', NewstylePluralIntl._meta.get_verbose_name())
+        self.assertEqual('newstyle plural intl', c._meta.get_verbose_name())
+
+        self.assertEqual('Modelos modernos traducibles #3', NewstylePluralIntl._meta.get_verbose_name(0))
+        self.assertEqual('Modelos modernos traducibles #3', c._meta.get_verbose_name(0))
