Index: django/db/models/fields/__init__.py
===================================================================
--- django/db/models/fields/__init__.py	(revision 11126)
+++ django/db/models/fields/__init__.py	(working copy)
@@ -59,6 +59,13 @@
     creation_counter = 0
     auto_creation_counter = -1
 
+    # Field type description
+    def _description(self):
+        return _(u'Field of type: %(field_type)s') % {
+            'field_type': self.__class__.__name__
+        }
+    description = property(_description)
+
     def __init__(self, verbose_name=None, name=None, primary_key=False,
             max_length=None, unique=False, blank=False, null=False,
             db_index=False, rel=None, default=NOT_PROVIDED, editable=True,
@@ -341,6 +348,8 @@
 
 class AutoField(Field):
     empty_strings_allowed = False
+    description = u"Integer"
+
     def __init__(self, *args, **kwargs):
         assert kwargs.get('primary_key', False) is True, "%ss must have primary_key=True." % self.__class__.__name__
         kwargs['blank'] = True
@@ -371,6 +380,8 @@
 
 class BooleanField(Field):
     empty_strings_allowed = False
+    description = u"Boolean (Either True or False)"
+
     def __init__(self, *args, **kwargs):
         kwargs['blank'] = True
         if 'default' not in kwargs and not kwargs.get('null'):
@@ -413,6 +424,8 @@
         return super(BooleanField, self).formfield(**defaults)
 
 class CharField(Field):
+    description = u"String (up to %(max_length)s)"
+
     def get_internal_type(self):
         return "CharField"
 
@@ -434,6 +447,8 @@
 
 # TODO: Maybe move this into contrib, because it's specialized.
 class CommaSeparatedIntegerField(CharField):
+    description = u"Comma-separated integers"
+
     def formfield(self, **kwargs):
         defaults = {
             'form_class': forms.RegexField,
@@ -450,6 +465,8 @@
 
 class DateField(Field):
     empty_strings_allowed = False
+    description = u"Date (without time)"
+
     def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs):
         self.auto_now, self.auto_now_add = auto_now, auto_now_add
         #HACKs : auto_now_add/auto_now should be done as a default or a pre_save.
@@ -524,6 +541,8 @@
         return super(DateField, self).formfield(**defaults)
 
 class DateTimeField(DateField):
+    description = u"Date (with time)"
+
     def get_internal_type(self):
         return "DateTimeField"
 
@@ -584,6 +603,8 @@
 
 class DecimalField(Field):
     empty_strings_allowed = False
+    description = u"Decimal number"
+
     def __init__(self, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs):
         self.max_digits, self.decimal_places = max_digits, decimal_places
         Field.__init__(self, verbose_name, name, **kwargs)
@@ -637,6 +658,8 @@
         return super(DecimalField, self).formfield(**defaults)
 
 class EmailField(CharField):
+    description = u"E-mail address"
+
     def __init__(self, *args, **kwargs):
         kwargs['max_length'] = kwargs.get('max_length', 75)
         CharField.__init__(self, *args, **kwargs)
@@ -647,6 +670,8 @@
         return super(EmailField, self).formfield(**defaults)
 
 class FilePathField(Field):
+    description = u"File path"
+
     def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs):
         self.path, self.match, self.recursive = path, match, recursive
         kwargs['max_length'] = kwargs.get('max_length', 100)
@@ -667,6 +692,7 @@
 
 class FloatField(Field):
     empty_strings_allowed = False
+    description = u"Floating point number"
 
     def get_db_prep_value(self, value):
         if value is None:
@@ -692,6 +718,8 @@
 
 class IntegerField(Field):
     empty_strings_allowed = False
+    description = u"Integer"
+
     def get_db_prep_value(self, value):
         if value is None:
             return None
@@ -716,6 +744,8 @@
 
 class IPAddressField(Field):
     empty_strings_allowed = False
+    description = u"IP address"
+
     def __init__(self, *args, **kwargs):
         kwargs['max_length'] = 15
         Field.__init__(self, *args, **kwargs)
@@ -730,6 +760,8 @@
 
 class NullBooleanField(Field):
     empty_strings_allowed = False
+    description = u"Boolean (Either True, False or None)"
+
     def __init__(self, *args, **kwargs):
         kwargs['null'] = True
         Field.__init__(self, *args, **kwargs)
@@ -769,6 +801,7 @@
         return super(NullBooleanField, self).formfield(**defaults)
 
 class PositiveIntegerField(IntegerField):
+
     def get_internal_type(self):
         return "PositiveIntegerField"
 
@@ -778,6 +811,7 @@
         return super(PositiveIntegerField, self).formfield(**defaults)
 
 class PositiveSmallIntegerField(IntegerField):
+
     def get_internal_type(self):
         return "PositiveSmallIntegerField"
 
@@ -787,6 +821,7 @@
         return super(PositiveSmallIntegerField, self).formfield(**defaults)
 
 class SlugField(CharField):
+
     def __init__(self, *args, **kwargs):
         kwargs['max_length'] = kwargs.get('max_length', 50)
         # Set db_index=True unless it's been set manually.
@@ -803,10 +838,13 @@
         return super(SlugField, self).formfield(**defaults)
 
 class SmallIntegerField(IntegerField):
+
     def get_internal_type(self):
         return "SmallIntegerField"
 
 class TextField(Field):
+    description = u"Text"
+
     def get_internal_type(self):
         return "TextField"
 
@@ -816,7 +854,10 @@
         return super(TextField, self).formfield(**defaults)
 
 class TimeField(Field):
+    description = u"Time"
+
     empty_strings_allowed = False
+
     def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs):
         self.auto_now, self.auto_now_add = auto_now, auto_now_add
         if auto_now or auto_now_add:
@@ -888,6 +929,8 @@
         return super(TimeField, self).formfield(**defaults)
 
 class URLField(CharField):
+    description = u"URL"
+
     def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs):
         kwargs['max_length'] = kwargs.get('max_length', 200)
         self.verify_exists = verify_exists
@@ -899,6 +942,8 @@
         return super(URLField, self).formfield(**defaults)
 
 class XMLField(TextField):
+    description = u"XML text"
+
     def __init__(self, verbose_name=None, name=None, schema_path=None, **kwargs):
         self.schema_path = schema_path
         Field.__init__(self, verbose_name, name, **kwargs)
Index: django/db/models/fields/related.py
===================================================================
--- django/db/models/fields/related.py	(revision 11126)
+++ django/db/models/fields/related.py	(working copy)
@@ -683,6 +683,8 @@
 
 class ForeignKey(RelatedField, Field):
     empty_strings_allowed = False
+    description = u"Foreign Key (type determined by related field)"
+
     def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
         try:
             to_name = to._meta.object_name.lower()
@@ -777,6 +779,9 @@
     always returns the object pointed to (since there will only ever be one),
     rather than returning a list.
     """
+
+    description = u"One-to-one relationship"
+
     def __init__(self, to, to_field=None, **kwargs):
         kwargs['unique'] = True
         super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs)
@@ -791,6 +796,8 @@
         return super(OneToOneField, self).formfield(**kwargs)
 
 class ManyToManyField(RelatedField, Field):
+    description = u"Many-to-many relationship"
+
     def __init__(self, to, **kwargs):
         try:
             assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
Index: django/db/models/fields/files.py
===================================================================
--- django/db/models/fields/files.py	(revision 11126)
+++ django/db/models/fields/files.py	(working copy)
@@ -209,6 +209,8 @@
         instance.__dict__[self.field.name] = value
 
 class FileField(Field):
+    description = u"File path"
+
     # The class to wrap instance attributes in. Accessing the file object off
     # the instance will always return an instance of attr_class.
     attr_class = FieldFile
@@ -323,6 +325,8 @@
         super(ImageFieldFile, self).delete(save)
 
 class ImageField(FileField):
+    description = u"File path"
+
     attr_class = ImageFieldFile
     descriptor_class = ImageFileDescriptor
 
Index: django/contrib/gis/db/models/fields/__init__.py
===================================================================
--- django/contrib/gis/db/models/fields/__init__.py	(revision 11126)
+++ django/contrib/gis/db/models/fields/__init__.py	(working copy)
@@ -30,14 +30,14 @@
     return _srid_cache[srid]
 
 class GeometryField(SpatialBackend.Field):
-    "The base GIS field -- maps to the OpenGIS Specification Geometry type."
-
     # The OpenGIS Geometry name.
     geom_type = 'GEOMETRY'
 
     # Geodetic units.
     geodetic_units = ('Decimal Degree', 'degree')
 
+    description = u"The base GIS field -- maps to the OpenGIS Specification Geometry type."
+
     def __init__(self, verbose_name=None, srid=4326, spatial_index=True, dim=2, **kwargs):
         """
         The initialization function for geometry fields.  Takes the following
@@ -258,21 +258,28 @@
 # The OpenGIS Geometry Type Fields
 class PointField(GeometryField):
     geom_type = 'POINT'
+    description = u"Point"
 
 class LineStringField(GeometryField):
     geom_type = 'LINESTRING'
+    description = u"Line string"
 
 class PolygonField(GeometryField):
     geom_type = 'POLYGON'
+    description = u"Polygon"
 
 class MultiPointField(GeometryField):
     geom_type = 'MULTIPOINT'
+    description = u"Multi-point"
 
 class MultiLineStringField(GeometryField):
     geom_type = 'MULTILINESTRING'
+    description = u"Multi-line string"
 
 class MultiPolygonField(GeometryField):
     geom_type = 'MULTIPOLYGON'
+    description = u"Multi-polygon"
 
 class GeometryCollectionField(GeometryField):
     geom_type = 'GEOMETRYCOLLECTION'
+    description = u"Geometry collection"
\ No newline at end of file
Index: django/contrib/admindocs/views.py
===================================================================
--- django/contrib/admindocs/views.py	(revision 11126)
+++ django/contrib/admindocs/views.py	(working copy)
@@ -179,7 +179,7 @@
 def model_detail(request, app_label, model_name):
     if not utils.docutils_is_available:
         return missing_docutils_page(request)
-        
+
     # Get the model class.
     try:
         app_mod = models.get_app(app_label)
@@ -307,44 +307,13 @@
             return 'Integer'
     return ''
 
-# Maps Field objects to their human-readable data types, as strings.
-# Column-type strings can contain format strings; they'll be interpolated
-# against the values of Field.__dict__ before being output.
-# If a column type is set to None, it won't be included in the output.
-DATA_TYPE_MAPPING = {
-    'AutoField'                 : _('Integer'),
-    'BooleanField'              : _('Boolean (Either True or False)'),
-    'CharField'                 : _('String (up to %(max_length)s)'),
-    'CommaSeparatedIntegerField': _('Comma-separated integers'),
-    'DateField'                 : _('Date (without time)'),
-    'DateTimeField'             : _('Date (with time)'),
-    'DecimalField'              : _('Decimal number'),
-    'EmailField'                : _('E-mail address'),
-    'FileField'                 : _('File path'),
-    'FilePathField'             : _('File path'),
-    'FloatField'                : _('Floating point number'),
-    'ForeignKey'                : _('Integer'),
-    'ImageField'                : _('File path'),
-    'IntegerField'              : _('Integer'),
-    'IPAddressField'            : _('IP address'),
-    'ManyToManyField'           : '',
-    'NullBooleanField'          : _('Boolean (Either True, False or None)'),
-    'OneToOneField'             : _('Relation to parent model'),
-    'PhoneNumberField'          : _('Phone number'),
-    'PositiveIntegerField'      : _('Integer'),
-    'PositiveSmallIntegerField' : _('Integer'),
-    'SlugField'                 : _('String (up to %(max_length)s)'),
-    'SmallIntegerField'         : _('Integer'),
-    'TextField'                 : _('Text'),
-    'TimeField'                 : _('Time'),
-    'URLField'                  : _('URL'),
-    'USStateField'              : _('U.S. state (two uppercase letters)'),
-    'XMLField'                  : _('XML text'),
-}
-
 def get_readable_field_data_type(field):
-    return DATA_TYPE_MAPPING[field.get_internal_type()] % field.__dict__
+    """Returns the description for a given field type, if it exists,
+    Fields' descriptions can contain format strings, which will be interpolated
+    against the values of field.__dict__ before being output."""
 
+    return _(field.description) % field.__dict__
+
 def extract_views_from_urlpatterns(urlpatterns, base=''):
     """
     Return a list of views from a list of urlpatterns.
Index: django/contrib/admindocs/tests.py
===================================================================
--- django/contrib/admindocs/tests.py	(revision 0)
+++ django/contrib/admindocs/tests.py	(revision 0)
@@ -0,0 +1,37 @@
+import unittest
+import views
+from django.db.models import fields as builtin_fields
+#import django.contrib.gis.db.models.fields as gis_fields
+import models
+
+
+class TestFieldType(unittest.TestCase):
+    def setUp(self):
+        pass
+
+    def test_field_name(self):
+        self.assertRaises(AttributeError,
+            views.get_readable_field_data_type, "NotAField"
+        )
+
+    def test_builtin_fields(self):
+        self.assertEqual(
+            views.get_readable_field_data_type(builtin_fields.BooleanField()),
+            u'Boolean (Either True or False)'
+        )
+
+        #def test_gis_fields(self):
+        #    self.assertEqual(
+        #        views.get_readable_field_data_type(gis_fields.PolygonField),
+        #        u'Polygon!'
+        #    )
+
+    def test_custom_fields(self):
+        self.assertEqual(
+            views.get_readable_field_data_type(models.CustomField()),
+            u'A custom field type'
+        )
+        self.assertEqual(
+            views.get_readable_field_data_type(models.DescriptionLackingField()),
+            u'Field of type: DescriptionLackingField'
+        )
Index: django/contrib/admindocs/models.py
===================================================================
--- django/contrib/admindocs/models.py	(revision 0)
+++ django/contrib/admindocs/models.py	(revision 0)
@@ -0,0 +1,9 @@
+"""Models for admindocs unittests."""
+
+from django.db import models
+
+class CustomField(models.Field):
+    description = u"A custom field type"
+
+class DescriptionLackingField(models.Field):
+    pass
Index: django/contrib/localflavor/us/models.py
===================================================================
--- django/contrib/localflavor/us/models.py	(revision 11126)
+++ django/contrib/localflavor/us/models.py	(working copy)
@@ -1,23 +1,27 @@
 from django.conf import settings
 from django.db.models.fields import Field
 
-class USStateField(Field): 
-    def get_internal_type(self): 
-        return "USStateField" 
-        
+class USStateField(Field):
+    description = u"U.S. state (two uppercase letters)"
+
+    def get_internal_type(self):
+        return "USStateField"
+
     def db_type(self):
         if settings.DATABASE_ENGINE == 'oracle':
             return 'CHAR(2)'
         else:
             return 'varchar(2)'
-    
-    def formfield(self, **kwargs): 
-        from django.contrib.localflavor.us.forms import USStateSelect 
-        defaults = {'widget': USStateSelect} 
-        defaults.update(kwargs) 
+
+    def formfield(self, **kwargs):
+        from django.contrib.localflavor.us.forms import USStateSelect
+        defaults = {'widget': USStateSelect}
+        defaults.update(kwargs)
         return super(USStateField, self).formfield(**defaults)
 
 class PhoneNumberField(Field):
+    description = u"Phone number"
+
     def get_internal_type(self):
         return "PhoneNumberField"
 
