Index: django/newforms/fields.py
===================================================================
--- django/newforms/fields.py	(revision 6094)
+++ django/newforms/fields.py	(working copy)
@@ -6,7 +6,7 @@
 import re
 import time
 
-from django.utils.translation import ugettext
+from django.utils.translation import ugettext_lazy
 from django.utils.encoding import StrAndUnicode, smart_unicode
 
 from util import ErrorList, ValidationError
@@ -44,11 +44,16 @@
 class Field(object):
     widget = TextInput # Default widget to use when rendering this type of Field.
     hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden".
+    default_error_messages = {
+        'required': ugettext_lazy(u'This field is required.'),
+        'invalid': ugettext_lazy(u'Enter a valid value.'),
+    }
 
     # Tracks each time a Field instance is created. Used to retain order.
     creation_counter = 0
 
-    def __init__(self, required=True, widget=None, label=None, initial=None, help_text=None):
+    def __init__(self, required=True, widget=None, label=None, initial=None,
+                 help_text=None, error_messages=None):
         # required -- Boolean that specifies whether the field is required.
         #             True by default.
         # widget -- A Widget class, or instance of a Widget class, that should
@@ -81,6 +86,20 @@
         self.creation_counter = Field.creation_counter
         Field.creation_counter += 1
 
+        self.error_messages = self._build_error_messages(error_messages)
+
+    def _build_error_messages(self, extra_error_messages):
+        error_messages = {}
+        def get_default_error_messages(klass):
+            for base_class in klass.__bases__:
+                get_default_error_messages(base_class)
+            if hasattr(klass, 'default_error_messages'):
+                error_messages.update(klass.default_error_messages)
+        get_default_error_messages(self.__class__)
+        if extra_error_messages:
+            error_messages.update(extra_error_messages)
+        return error_messages
+
     def clean(self, value):
         """
         Validates the given value and returns its "cleaned" value as an
@@ -89,7 +108,7 @@
         Raises ValidationError for any errors.
         """
         if self.required and value in EMPTY_VALUES:
-            raise ValidationError(ugettext(u'This field is required.'))
+            raise ValidationError(self.error_messages['required'])
         return value
 
     def widget_attrs(self, widget):
@@ -101,6 +120,11 @@
         return {}
 
 class CharField(Field):
+    default_error_messages = {
+        'max_length': ugettext_lazy(u'Ensure this value has at most %(max)d characters (it has %(length)d).'),
+        'min_length': ugettext_lazy(u'Ensure this value has at least %(min)d characters (it has %(length)d).'),
+    }
+
     def __init__(self, max_length=None, min_length=None, *args, **kwargs):
         self.max_length, self.min_length = max_length, min_length
         super(CharField, self).__init__(*args, **kwargs)
@@ -113,9 +137,9 @@
         value = smart_unicode(value)
         value_length = len(value)
         if self.max_length is not None and value_length > self.max_length:
-            raise ValidationError(ugettext(u'Ensure this value has at most %(max)d characters (it has %(length)d).') % {'max': self.max_length, 'length': value_length})
+            raise ValidationError(self.error_messages['max_length'] % {'max': self.max_length, 'length': value_length})
         if self.min_length is not None and value_length < self.min_length:
-            raise ValidationError(ugettext(u'Ensure this value has at least %(min)d characters (it has %(length)d).') % {'min': self.min_length, 'length': value_length})
+            raise ValidationError(self.error_messages['min_length'] % {'min': self.min_length, 'length': value_length})
         return value
 
     def widget_attrs(self, widget):
@@ -124,6 +148,12 @@
             return {'maxlength': str(self.max_length)}
 
 class IntegerField(Field):
+    default_error_messages = {
+        'invalid': ugettext_lazy(u'Enter a whole number.'),
+        'max_value': ugettext_lazy(u'Ensure this value is less than or equal to %s.'),
+        'min_value': ugettext_lazy(u'Ensure this value is greater than or equal to %s.'),
+    }
+
     def __init__(self, max_value=None, min_value=None, *args, **kwargs):
         self.max_value, self.min_value = max_value, min_value
         super(IntegerField, self).__init__(*args, **kwargs)
@@ -139,14 +169,20 @@
         try:
             value = int(value)
         except (ValueError, TypeError):
-            raise ValidationError(ugettext(u'Enter a whole number.'))
+            raise ValidationError(self.error_messages['invalid'])
         if self.max_value is not None and value > self.max_value:
-            raise ValidationError(ugettext(u'Ensure this value is less than or equal to %s.') % self.max_value)
+            raise ValidationError(self.error_messages['max_value'] % self.max_value)
         if self.min_value is not None and value < self.min_value:
-            raise ValidationError(ugettext(u'Ensure this value is greater than or equal to %s.') % self.min_value)
+            raise ValidationError(self.error_messages['min_value'] % self.min_value)
         return value
 
 class FloatField(Field):
+    default_error_messages = {
+        'invalid': ugettext_lazy(u'Enter a number.'),
+        'max_value': ugettext_lazy(u'Ensure this value is less than or equal to %s.'),
+        'min_value': ugettext_lazy(u'Ensure this value is greater than or equal to %s.'),
+    }
+
     def __init__(self, max_value=None, min_value=None, *args, **kwargs):
         self.max_value, self.min_value = max_value, min_value
         Field.__init__(self, *args, **kwargs)
@@ -162,14 +198,23 @@
         try:
             value = float(value)
         except (ValueError, TypeError):
-            raise ValidationError(ugettext('Enter a number.'))
+            raise ValidationError(self.error_messages['invalid'])
         if self.max_value is not None and value > self.max_value:
-            raise ValidationError(ugettext('Ensure this value is less than or equal to %s.') % self.max_value)
+            raise ValidationError(self.error_messages['max_value'] % self.max_value)
         if self.min_value is not None and value < self.min_value:
-            raise ValidationError(ugettext('Ensure this value is greater than or equal to %s.') % self.min_value)
+            raise ValidationError(self.error_messages['min_value'] % self.min_value)
         return value
 
 class DecimalField(Field):
+    default_error_messages = {
+        'invalid': ugettext_lazy(u'Enter a number.'),
+        'max_value': ugettext_lazy(u'Ensure this value is less than or equal to %s.'),
+        'min_value': ugettext_lazy(u'Ensure this value is greater than or equal to %s.'),
+        'max_digits': ugettext_lazy('Ensure that there are no more than %s digits in total.'),
+        'max_decimal_places': ugettext_lazy('Ensure that there are no more than %s decimal places.'),
+        'max_whole_digits': ugettext_lazy('Ensure that there are no more than %s digits before the decimal point.')
+    }
+
     def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
         self.max_value, self.min_value = max_value, min_value
         self.max_digits, self.decimal_places = max_digits, decimal_places
@@ -189,20 +234,20 @@
         try:
             value = Decimal(value)
         except DecimalException:
-            raise ValidationError(ugettext('Enter a number.'))
+            raise ValidationError(self.error_messages['invalid'])
         pieces = str(value).lstrip("-").split('.')
         decimals = (len(pieces) == 2) and len(pieces[1]) or 0
         digits = len(pieces[0])
         if self.max_value is not None and value > self.max_value:
-            raise ValidationError(ugettext('Ensure this value is less than or equal to %s.') % self.max_value)
+            raise ValidationError(self.error_messages['max_value'] % self.max_value)
         if self.min_value is not None and value < self.min_value:
-            raise ValidationError(ugettext('Ensure this value is greater than or equal to %s.') % self.min_value)
+            raise ValidationError(self.error_messages['min_value'] % self.min_value)
         if self.max_digits is not None and (digits + decimals) > self.max_digits:
-            raise ValidationError(ugettext('Ensure that there are no more than %s digits in total.') % self.max_digits)
+            raise ValidationError(self.error_messages['max_digits'] % self.max_digits)
         if self.decimal_places is not None and decimals > self.decimal_places:
-            raise ValidationError(ugettext('Ensure that there are no more than %s decimal places.') % self.decimal_places)
+            raise ValidationError(self.error_messages['max_decimal_places'] % self.decimal_places)
         if self.max_digits is not None and self.decimal_places is not None and digits > (self.max_digits - self.decimal_places):
-            raise ValidationError(ugettext('Ensure that there are no more than %s digits before the decimal point.') % (self.max_digits - self.decimal_places))
+            raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places))
         return value
 
 DEFAULT_DATE_INPUT_FORMATS = (
@@ -214,6 +259,10 @@
 )
 
 class DateField(Field):
+    default_error_messages = {
+        'invalid': ugettext_lazy(u'Enter a valid date.'),
+    }
+
     def __init__(self, input_formats=None, *args, **kwargs):
         super(DateField, self).__init__(*args, **kwargs)
         self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS
@@ -235,7 +284,7 @@
                 return datetime.date(*time.strptime(value, format)[:3])
             except ValueError:
                 continue
-        raise ValidationError(ugettext(u'Enter a valid date.'))
+        raise ValidationError(self.error_messages['invalid'])
 
 DEFAULT_TIME_INPUT_FORMATS = (
     '%H:%M:%S',     # '14:30:59'
@@ -243,6 +292,10 @@
 )
 
 class TimeField(Field):
+    default_error_messages = {
+        'invalid': ugettext_lazy(u'Enter a valid time.')
+    }
+
     def __init__(self, input_formats=None, *args, **kwargs):
         super(TimeField, self).__init__(*args, **kwargs)
         self.input_formats = input_formats or DEFAULT_TIME_INPUT_FORMATS
@@ -262,7 +315,7 @@
                 return datetime.time(*time.strptime(value, format)[3:6])
             except ValueError:
                 continue
-        raise ValidationError(ugettext(u'Enter a valid time.'))
+        raise ValidationError(self.error_messages['invalid'])
 
 DEFAULT_DATETIME_INPUT_FORMATS = (
     '%Y-%m-%d %H:%M:%S',     # '2006-10-25 14:30:59'
@@ -277,6 +330,10 @@
 )
 
 class DateTimeField(Field):
+    default_error_messages = {
+        'invalid': ugettext_lazy(u'Enter a valid date/time.'),
+    }
+
     def __init__(self, input_formats=None, *args, **kwargs):
         super(DateTimeField, self).__init__(*args, **kwargs)
         self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS
@@ -298,7 +355,7 @@
                 return datetime.datetime(*time.strptime(value, format)[:6])
             except ValueError:
                 continue
-        raise ValidationError(ugettext(u'Enter a valid date/time.'))
+        raise ValidationError(self.error_messages['invalid'])
 
 class RegexField(CharField):
     def __init__(self, regex, max_length=None, min_length=None, error_message=None, *args, **kwargs):
@@ -307,11 +364,15 @@
         error_message is an optional error message to use, if
         'Enter a valid value' is too generic for you.
         """
+        # error_message is just kept for backwards compatibility:
+        if error_message:
+            error_messages = kwargs.get('error_messages') or {}
+            error_messages['invalid'] = error_message
+            kwargs['error_messages'] = error_messages
         super(RegexField, self).__init__(max_length, min_length, *args, **kwargs)
         if isinstance(regex, basestring):
             regex = re.compile(regex)
         self.regex = regex
-        self.error_message = error_message or ugettext(u'Enter a valid value.')
 
     def clean(self, value):
         """
@@ -322,7 +383,7 @@
         if value == u'':
             return value
         if not self.regex.search(value):
-            raise ValidationError(self.error_message)
+            raise ValidationError(self.error_messages['invalid'])
         return value
 
 email_re = re.compile(
@@ -331,9 +392,13 @@
     r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE)  # domain
 
 class EmailField(RegexField):
+    default_error_messages = {
+        'invalid': ugettext_lazy(u'Enter a valid e-mail address.'),
+    }
+
     def __init__(self, max_length=None, min_length=None, *args, **kwargs):
-        RegexField.__init__(self, email_re, max_length, min_length,
-            ugettext(u'Enter a valid e-mail address.'), *args, **kwargs)
+        RegexField.__init__(self, email_re, max_length, min_length, *args,
+                            **kwargs)
 
 url_re = re.compile(
     r'^https?://' # http:// or https://
@@ -363,6 +428,12 @@
 
 class FileField(Field):
     widget = FileInput
+    default_error_messages = {
+        'invalid': ugettext_lazy(u"No file was submitted. Check the encoding type on the form."),
+        'missing': ugettext_lazy(u"No file was submitted."),
+        'empty': ugettext_lazy(u"The submitted file is empty."),
+    }
+
     def __init__(self, *args, **kwargs):
         super(FileField, self).__init__(*args, **kwargs)
 
@@ -373,14 +444,18 @@
         try:
             f = UploadedFile(data['filename'], data['content'])
         except TypeError:
-            raise ValidationError(ugettext(u"No file was submitted. Check the encoding type on the form."))
+            raise ValidationError(self.error_messages['invalid'])
         except KeyError:
-            raise ValidationError(ugettext(u"No file was submitted."))
+            raise ValidationError(self.error_messages['missing'])
         if not f.content:
-            raise ValidationError(ugettext(u"The submitted file is empty."))
+            raise ValidationError(self.error_messages['empty'])
         return f
 
 class ImageField(FileField):
+    default_error_messages = {
+        'invalid_image': ugettext_lazy(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."),
+    }
+
     def clean(self, data):
         """
         Checks that the file-upload field data contains a valid image (GIF, JPG,
@@ -394,13 +469,19 @@
         try:
             Image.open(StringIO(f.content))
         except IOError: # Python Imaging Library doesn't recognize it as an image
-            raise ValidationError(ugettext(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."))
+            raise ValidationError(self.error_messages['invalid_image'])
         return f
 
 class URLField(RegexField):
+    default_error_messages = {
+        'invalid': ugettext_lazy(u'Enter a valid URL.'),
+        'invalid_link': ugettext_lazy(u'This URL appears to be a broken link.'),
+    }
+
     def __init__(self, max_length=None, min_length=None, verify_exists=False,
             validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs):
-        super(URLField, self).__init__(url_re, max_length, min_length, ugettext(u'Enter a valid URL.'), *args, **kwargs)
+        super(URLField, self).__init__(url_re, max_length, min_length, *args,
+                                       **kwargs)
         self.verify_exists = verify_exists
         self.user_agent = validator_user_agent
 
@@ -422,9 +503,9 @@
                 req = urllib2.Request(value, None, headers)
                 u = urllib2.urlopen(req)
             except ValueError:
-                raise ValidationError(ugettext(u'Enter a valid URL.'))
+                raise ValidationError(self.error_messages['invalid'])
             except: # urllib2.URLError, httplib.InvalidURL, etc.
-                raise ValidationError(ugettext(u'This URL appears to be a broken link.'))
+                raise ValidationError(self.error_messages['invalid_link'])
         return value
 
 class BooleanField(Field):
@@ -447,9 +528,14 @@
 
 class ChoiceField(Field):
     widget = Select
+    default_error_messages = {
+        'invalid_choice': ugettext_lazy(u'Select a valid choice. That choice is not one of the available choices.'),
+    }
 
-    def __init__(self, choices=(), required=True, widget=None, label=None, initial=None, help_text=None):
-        super(ChoiceField, self).__init__(required, widget, label, initial, help_text)
+    def __init__(self, choices=(), required=True, widget=None, label=None,
+                 initial=None, help_text=None, *args, **kwargs):
+        super(ChoiceField, self).__init__(required, widget, label, initial,
+                                          help_text, *args, **kwargs)
         self.choices = choices
 
     def _get_choices(self):
@@ -475,29 +561,33 @@
             return value
         valid_values = set([smart_unicode(k) for k, v in self.choices])
         if value not in valid_values:
-            raise ValidationError(ugettext(u'Select a valid choice. That choice is not one of the available choices.'))
+            raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
         return value
 
 class MultipleChoiceField(ChoiceField):
     hidden_widget = MultipleHiddenInput
     widget = SelectMultiple
+    default_error_messages = {
+        'invalid_choice': ugettext_lazy(u'Select a valid choice. %(value)s is not one of the available choices.'),
+        'invalid_list': ugettext_lazy(u'Enter a list of values.'),
+    }
 
     def clean(self, value):
         """
         Validates that the input is a list or tuple.
         """
         if self.required and not value:
-            raise ValidationError(ugettext(u'This field is required.'))
+            raise ValidationError(self.error_messages['required'])
         elif not self.required and not value:
             return []
         if not isinstance(value, (list, tuple)):
-            raise ValidationError(ugettext(u'Enter a list of values.'))
+            raise ValidationError(self.error_messages['invalid_list'])
         new_value = [smart_unicode(val) for val in value]
         # Validate that each value in the value list is in self.choices.
         valid_values = set([smart_unicode(k) for k, v in self.choices])
         for val in new_value:
             if val not in valid_values:
-                raise ValidationError(ugettext(u'Select a valid choice. %s is not one of the available choices.') % val)
+                raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
         return new_value
 
 class ComboField(Field):
@@ -540,6 +630,9 @@
 
     You'll probably want to use this with MultiWidget.
     """
+    default_error_messages = {
+        'invalid': ugettext_lazy(u'Enter a list of values.'),
+    }
     def __init__(self, fields=(), *args, **kwargs):
         super(MultiValueField, self).__init__(*args, **kwargs)
         # Set 'required' to False on the individual fields, because the
@@ -563,18 +656,18 @@
         if not value or isinstance(value, (list, tuple)):
             if not value or not [v for v in value if v not in EMPTY_VALUES]:
                 if self.required:
-                    raise ValidationError(ugettext(u'This field is required.'))
+                    raise ValidationError(self.error_messages['required'])
                 else:
                     return self.compress([])
         else:
-            raise ValidationError(ugettext(u'Enter a list of values.'))
+            raise ValidationError(self.error_messages['invalid'])
         for i, field in enumerate(self.fields):
             try:
                 field_value = value[i]
             except IndexError:
                 field_value = None
             if self.required and field_value in EMPTY_VALUES:
-                raise ValidationError(ugettext(u'This field is required.'))
+                raise ValidationError(self.error_messages['required'])
             try:
                 clean_data.append(field.clean(field_value))
             except ValidationError, e:
@@ -598,8 +691,16 @@
         raise NotImplementedError('Subclasses must implement this method.')
 
 class SplitDateTimeField(MultiValueField):
+    default_error_messages = {
+        'invalid_date': ugettext_lazy(u'Enter a valid date.'),
+        'invalid_time': ugettext_lazy(u'Enter a valid time.'),
+    }
     def __init__(self, *args, **kwargs):
-        fields = (DateField(), TimeField())
+        e = self.default_error_messages.copy()
+        if 'error_messages' in kwargs:
+            e.update(kwargs['error_messages'])
+        fields = (DateField(error_messages={'invalid': e['invalid_date']}),
+                  TimeField(error_messages={'invalid': e['invalid_time']}))
         super(SplitDateTimeField, self).__init__(fields, *args, **kwargs)
 
     def compress(self, data_list):
@@ -607,8 +708,8 @@
             # Raise a validation error if time or date is empty
             # (possible if SplitDateTimeField has required=False).
             if data_list[0] in EMPTY_VALUES:
-                raise ValidationError(ugettext(u'Enter a valid date.'))
+                raise ValidationError(self.error_messages['invalid_date'])
             if data_list[1] in EMPTY_VALUES:
-                raise ValidationError(ugettext(u'Enter a valid time.'))
+                raise ValidationError(self.error_messages['invalid_time'])
             return datetime.datetime.combine(*data_list)
         return None
Index: django/newforms/util.py
===================================================================
--- django/newforms/util.py	(revision 6094)
+++ django/newforms/util.py	(working copy)
@@ -1,5 +1,5 @@
 from django.utils.html import escape
-from django.utils.encoding import smart_unicode, StrAndUnicode
+from django.utils.encoding import smart_unicode, force_unicode, StrAndUnicode
 
 def flatatt(attrs):
     """
@@ -41,13 +41,15 @@
         if not self: return u''
         return u'\n'.join([u'* %s' % smart_unicode(e) for e in self])
 
+    def __repr__(self):
+        return repr([force_unicode(e) for e in self])
+
 class ValidationError(Exception):
     def __init__(self, message):
         "ValidationError can be passed a string or a list."
         if isinstance(message, list):
             self.messages = ErrorList([smart_unicode(msg) for msg in message])
         else:
-            assert isinstance(message, basestring), ("%s should be a basestring" % repr(message))
             message = smart_unicode(message)
             self.messages = ErrorList([message])
 
Index: docs/newforms.txt
===================================================================
--- docs/newforms.txt	(revision 6094)
+++ docs/newforms.txt	(working copy)
@@ -997,6 +997,28 @@
     <p>Sender: <input type="text" name="sender" /> A valid e-mail address, please.</p>
     <p>Cc myself: <input type="checkbox" name="cc_myself" /></p>
 
+``error_messages``
+~~~~~~~~~~~~~~~~~~
+
+The ``error_messages`` argument lets you override the default messages which the
+field will raise. Pass in a dictionary with keys matching the error messages you
+want to override. For example::
+
+    >>> generic = forms.CharField()
+    >>> generic.clean('')
+    Traceback (most recent call last):
+      ...
+    ValidationError: [u'This field is required.']
+
+    >>> name = forms.CharField(error_messages={'required': 'Please enter your name'})
+    >>> name.clean('')
+    Traceback (most recent call last):
+      ...
+    ValidationError: [u'Please enter your name']
+
+In the `built-in Field classes`_ section below, each Field defines the error
+message keys it uses.  
+
 Dynamic initial values
 ----------------------
 
@@ -1061,6 +1083,7 @@
     * Empty value: ``None``
     * Normalizes to: A Python ``True`` or ``False`` value.
     * Validates nothing (i.e., it never raises a ``ValidationError``).
+    * Error message keys: ``required``
 
 ``CharField``
 ~~~~~~~~~~~~~
@@ -1069,6 +1092,7 @@
     * Empty value: ``''`` (an empty string)
     * Normalizes to: A Unicode object.
     * Validates nothing, unless ``max_length`` or ``min_length`` is provided.
+    * Error message keys: ``required``, ``max_length``, ``min_length``
 
 Has two optional arguments for validation, ``max_length`` and ``min_length``.
 If provided, these arguments ensure that the string is at most or at least the
@@ -1081,6 +1105,7 @@
     * Empty value: ``''`` (an empty string)
     * Normalizes to: A Unicode object.
     * Validates that the given value exists in the list of choices.
+    * Error message keys: ``required``, ``invalid_choice``
 
 Takes one extra argument, ``choices``, which is an iterable (e.g., a list or
 tuple) of 2-tuples to use as choices for this field.
@@ -1093,6 +1118,7 @@
     * Normalizes to: A Python ``datetime.date`` object.
     * Validates that the given value is either a ``datetime.date``,
       ``datetime.datetime`` or string formatted in a particular date format.
+    * Error message keys: ``required``, ``invalid``
 
 Takes one optional argument, ``input_formats``, which is a list of formats used
 to attempt to convert a string to a valid ``datetime.date`` object.
@@ -1113,6 +1139,7 @@
     * Normalizes to: A Python ``datetime.datetime`` object.
     * Validates that the given value is either a ``datetime.datetime``,
       ``datetime.date`` or string formatted in a particular datetime format.
+    * Error message keys: ``required``, ``invalid``
 
 Takes one optional argument, ``input_formats``, which is a list of formats used
 to attempt to convert a string to a valid ``datetime.datetime`` object.
@@ -1139,6 +1166,9 @@
     * Normalizes to: A Python ``decimal``.
     * Validates that the given value is a decimal. Leading and trailing
       whitespace is ignored.
+    * Error message keys: ``required``, ``invalid``, ``max_value``,
+      ``min_value``, ``max_digits``, ``max_decimal_places``,
+      ``max_whole_digits``
 
 Takes four optional arguments: ``max_value``, ``min_value``, ``max_digits``,
 and ``decimal_places``. The first two define the limits for the fields value.
@@ -1155,6 +1185,7 @@
     * Normalizes to: A Unicode object.
     * Validates that the given value is a valid e-mail address, using a
       moderately complex regular expression.
+    * Error message keys: ``required``, ``invalid``
 
 Has two optional arguments for validation, ``max_length`` and ``min_length``.
 If provided, these arguments ensure that the string is at most or at least the
@@ -1170,6 +1201,7 @@
     * Normalizes to: An ``UploadedFile`` object that wraps the file content
       and file name into a single object.
     * Validates that non-empty file data has been bound to the form.
+    * Error message keys: ``required``, ``invalid``, ``missing``, ``empty``
 
 An ``UploadedFile`` object has two attributes:
 
@@ -1200,6 +1232,8 @@
       and file name into a single object.
     * Validates that file data has been bound to the form, and that the
       file is of an image format understood by PIL.
+    * Error message keys: ``required``, ``invalid``, ``missing``, ``empty``,
+      ``invalid_image``
 
 Using an ImageField requires that the `Python Imaging Library`_ is installed.
 
@@ -1216,6 +1250,8 @@
     * Normalizes to: A Python integer or long integer.
     * Validates that the given value is an integer. Leading and trailing
       whitespace is allowed, as in Python's ``int()`` function.
+    * Error message keys: ``required``, ``invalid``, ``max_value``,
+      ``min_value``
 
 Takes two optional arguments for validation, ``max_value`` and ``min_value``.
 These control the range of values permitted in the field.
@@ -1228,6 +1264,7 @@
     * Normalizes to: A list of Unicode objects.
     * Validates that every value in the given list of values exists in the list
       of choices.
+    * Error message keys: ``required``, ``invalid_choice``, ``invalid_list``
 
 Takes one extra argument, ``choices``, which is an iterable (e.g., a list or
 tuple) of 2-tuples to use as choices for this field.
@@ -1248,6 +1285,7 @@
     * Normalizes to: A Unicode object.
     * Validates that the given value matches against a certain regular
       expression.
+    * Error message keys: ``required``, ``invalid``
 
 Takes one required argument, ``regex``, which is a regular expression specified
 either as a string or a compiled regular expression object.
@@ -1259,11 +1297,13 @@
     ======================  =====================================================
     ``max_length``          Ensures the string has at most this many characters.
     ``min_length``          Ensures the string has at least this many characters.
-    ``error_message``       Error message to return for failed validation. If no
-                            message is provided, a generic error message will be
-                            used.
     ======================  =====================================================
 
+The optional argument ``error_message`` is also accepted for backwards
+compatibility. The preferred way to provide an error message is to use the
+``error_messages`` argument, passing a dictionary with ``'invalid'`` as a key
+and the error message as the value. 
+
 ``TimeField``
 ~~~~~~~~~~~~~
 
@@ -1272,6 +1312,7 @@
     * Normalizes to: A Python ``datetime.time`` object.
     * Validates that the given value is either a ``datetime.time`` or string
       formatted in a particular time format.
+    * Error message keys: ``required``, ``invalid``
 
 Takes one optional argument, ``input_formats``, which is a list of formats used
 to attempt to convert a string to a valid ``datetime.time`` object.
@@ -1288,6 +1329,7 @@
     * Empty value: ``''`` (an empty string)
     * Normalizes to: A Unicode object.
     * Validates that the given value is a valid URL.
+    * Error message keys: ``required``, ``invalid``, ``invalid_link``
 
 Takes the following optional arguments:
 
Index: tests/regressiontests/forms/tests.py
===================================================================
--- tests/regressiontests/forms/tests.py	(revision 6094)
+++ tests/regressiontests/forms/tests.py	(working copy)
@@ -939,6 +939,24 @@
 >>> f.clean('1234567890a')
 u'1234567890a'
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s'
+>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s'
+>>> f = CharField(min_length=5, max_length=10, error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+>>> f.clean('1234')
+Traceback (most recent call last):
+...
+ValidationError: [u'LENGTH 4, MIN LENGTH 5']
+>>> f.clean('12345678901')
+Traceback (most recent call last):
+...
+ValidationError: [u'LENGTH 11, MAX LENGTH 10']
+
 # IntegerField ################################################################
 
 >>> f = IntegerField()
@@ -1064,6 +1082,29 @@
 ...
 ValidationError: [u'Ensure this value is less than or equal to 20.']
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> e['invalid'] = 'INVALID'
+>>> e['min_value'] = 'MIN VALUE IS %s'
+>>> e['max_value'] = 'MAX VALUE IS %s'
+>>> f = IntegerField(min_value=5, max_value=10, error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+>>> f.clean('abc')
+Traceback (most recent call last):
+...
+ValidationError: [u'INVALID']
+>>> f.clean('4')
+Traceback (most recent call last):
+...
+ValidationError: [u'MIN VALUE IS 5']
+>>> f.clean('11')
+Traceback (most recent call last):
+...
+ValidationError: [u'MAX VALUE IS 10']
+
 # FloatField ##################################################################
 
 >>> f = FloatField()
@@ -1122,6 +1163,29 @@
 >>> f.clean('0.5')
 0.5
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> e['invalid'] = 'INVALID'
+>>> e['min_value'] = 'MIN VALUE IS %s'
+>>> e['max_value'] = 'MAX VALUE IS %s'
+>>> f = FloatField(min_value=5, max_value=10, error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+>>> f.clean('abc')
+Traceback (most recent call last):
+...
+ValidationError: [u'INVALID']
+>>> f.clean('4')
+Traceback (most recent call last):
+...
+ValidationError: [u'MIN VALUE IS 5']
+>>> f.clean('11')
+Traceback (most recent call last):
+...
+ValidationError: [u'MAX VALUE IS 10']
+
 # DecimalField ################################################################
 
 >>> f = DecimalField(max_digits=4, decimal_places=2)
@@ -1220,6 +1284,45 @@
 >>> f.clean('00.50')
 Decimal("0.50")
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> e['invalid'] = 'INVALID'
+>>> e['min_value'] = 'MIN VALUE IS %s'
+>>> e['max_value'] = 'MAX VALUE IS %s'
+>>> e['max_digits'] = 'MAX DIGITS IS %s'
+>>> e['max_decimal_places'] = 'MAX DP IS %s'
+>>> e['max_whole_digits'] = 'MAX DIGITS BEFORE DP IS %s'
+>>> f = DecimalField(min_value=5, max_value=10, error_messages=e)
+>>> f2 = DecimalField(max_digits=4, decimal_places=2, error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+>>> f.clean('abc')
+Traceback (most recent call last):
+...
+ValidationError: [u'INVALID']
+>>> f.clean('4')
+Traceback (most recent call last):
+...
+ValidationError: [u'MIN VALUE IS 5']
+>>> f.clean('11')
+Traceback (most recent call last):
+...
+ValidationError: [u'MAX VALUE IS 10']
+>>> f2.clean('123.45')
+Traceback (most recent call last):
+...
+ValidationError: [u'MAX DIGITS IS 4']
+>>> f2.clean('1.234')
+Traceback (most recent call last):
+...
+ValidationError: [u'MAX DP IS 2']
+>>> f2.clean('123.4')
+Traceback (most recent call last):
+...
+ValidationError: [u'MAX DIGITS BEFORE DP IS 2']
+
 # DateField ###################################################################
 
 >>> import datetime
@@ -1297,6 +1400,19 @@
 ...
 ValidationError: [u'Enter a valid date.']
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> e['invalid'] = 'INVALID'
+>>> f = DateField(error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+>>> f.clean('abc')
+Traceback (most recent call last):
+...
+ValidationError: [u'INVALID']
+
 # TimeField ###################################################################
 
 >>> import datetime
@@ -1336,6 +1452,19 @@
 ...
 ValidationError: [u'Enter a valid time.']
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> e['invalid'] = 'INVALID'
+>>> f = TimeField(error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+>>> f.clean('abc')
+Traceback (most recent call last):
+...
+ValidationError: [u'INVALID']
+
 # DateTimeField ###############################################################
 
 >>> import datetime
@@ -1409,6 +1538,19 @@
 >>> repr(f.clean(''))
 'None'
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> e['invalid'] = 'INVALID'
+>>> f = DateTimeField(error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+>>> f.clean('abc')
+Traceback (most recent call last):
+...
+ValidationError: [u'INVALID']
+
 # RegexField ##################################################################
 
 >>> f = RegexField('^\d[A-F]\d$')
@@ -1500,6 +1642,29 @@
 ...
 ValidationError: [u'Enter a valid value.']
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> e['invalid'] = 'INVALID'
+>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s'
+>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s'
+>>> f = RegexField(r'^\d+$', min_length=5, max_length=10, error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+>>> f.clean('abcde')
+Traceback (most recent call last):
+...
+ValidationError: [u'INVALID']
+>>> f.clean('1234')
+Traceback (most recent call last):
+...
+ValidationError: [u'LENGTH 4, MIN LENGTH 5']
+>>> f.clean('12345678901')
+Traceback (most recent call last):
+...
+ValidationError: [u'LENGTH 11, MAX LENGTH 10']
+
 # EmailField ##################################################################
 
 >>> f = EmailField()
@@ -1559,6 +1724,29 @@
 ...
 ValidationError: [u'Ensure this value has at most 15 characters (it has 20).']
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> e['invalid'] = 'INVALID'
+>>> e['min_length'] = 'LENGTH %(length)s, MIN LENGTH %(min)s'
+>>> e['max_length'] = 'LENGTH %(length)s, MAX LENGTH %(max)s'
+>>> f = EmailField(min_length=8, max_length=10, error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+>>> f.clean('abcdefgh')
+Traceback (most recent call last):
+...
+ValidationError: [u'INVALID']
+>>> f.clean('a@b.com')
+Traceback (most recent call last):
+...
+ValidationError: [u'LENGTH 7, MIN LENGTH 8']
+>>> f.clean('aye@bee.com')
+Traceback (most recent call last):
+...
+ValidationError: [u'LENGTH 11, MAX LENGTH 10']
+
 # FileField ##################################################################
 
 >>> f = FileField()
@@ -1595,6 +1783,29 @@
 >>> type(f.clean({'filename': 'name', 'content':'Some File Content'}))
 <class 'django.newforms.fields.UploadedFile'>
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> e['invalid'] = 'INVALID'
+>>> e['missing'] = 'MISSING'
+>>> e['empty'] = 'EMPTY FILE'
+>>> f = FileField(error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+>>> f.clean('abc')
+Traceback (most recent call last):
+...
+ValidationError: [u'INVALID']
+>>> f.clean({})
+Traceback (most recent call last):
+...
+ValidationError: [u'MISSING']
+>>> f.clean({'filename': 'name', 'content':''})
+Traceback (most recent call last):
+...
+ValidationError: [u'EMPTY FILE']
+
 # URLField ##################################################################
 
 >>> f = URLField()
@@ -1705,6 +1916,24 @@
 ...
 ValidationError: [u'Ensure this value has at most 20 characters (it has 37).']
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> e['invalid'] = 'INVALID'
+>>> e['invalid_link'] = 'INVALID LINK'
+>>> f = URLField(verify_exists=True, error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+>>> f.clean('abc.c')
+Traceback (most recent call last):
+...
+ValidationError: [u'INVALID']
+>>> f.clean('http://www.jfoiwjfoi23jfoijoaijfoiwjofiwjefewl.com')
+Traceback (most recent call last):
+...
+ValidationError: [u'INVALID LINK']
+
 # BooleanField ################################################################
 
 >>> f = BooleanField()
@@ -1743,6 +1972,14 @@
 >>> f.clean('Django rocks')
 True
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> f = BooleanField(error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+
 # ChoiceField #################################################################
 
 >>> f = ChoiceField(choices=[('1', '1'), ('2', '2')])
@@ -1785,6 +2022,19 @@
 ...
 ValidationError: [u'Select a valid choice. That choice is not one of the available choices.']
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> e['invalid_choice'] = '%(value)s IS INVALID CHOICE'
+>>> f = ChoiceField(choices=[('a', 'aye')], error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+>>> f.clean('b')
+Traceback (most recent call last):
+...
+ValidationError: [u'b IS INVALID CHOICE']
+
 # NullBooleanField ############################################################
 
 >>> f = NullBooleanField()
@@ -1865,6 +2115,24 @@
 ...
 ValidationError: [u'Select a valid choice. 3 is not one of the available choices.']
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> e['invalid_choice'] = '%(value)s IS INVALID CHOICE'
+>>> e['invalid_list'] = 'NOT A LIST'
+>>> f = MultipleChoiceField(choices=[('a', 'aye')], error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+>>> f.clean('b')
+Traceback (most recent call last):
+...
+ValidationError: [u'NOT A LIST']
+>>> f.clean(['b'])
+Traceback (most recent call last):
+...
+ValidationError: [u'b IS INVALID CHOICE']
+
 # ComboField ##################################################################
 
 ComboField takes a list of fields that should be used to validate a value,
@@ -1973,6 +2241,20 @@
 ...
 ValidationError: [u'Enter a valid date.']
 
+Custom error messages
+>>> e = {'required': 'REQUIRED'}
+>>> e['invalid_date'] = 'INVALID DATE'
+>>> e['invalid_time'] = 'INVALID TIME'
+>>> f = SplitDateTimeField(error_messages=e)
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'REQUIRED']
+>>> f.clean(['a', 'b'])
+Traceback (most recent call last):
+...
+ValidationError: [u'INVALID DATE', u'INVALID TIME']
+
 #########
 # Forms #
 #########
@@ -3821,6 +4103,7 @@
 True
 >>> f.cleaned_data['username']
 u'sirrobin'
+
 """
 
 __test__ = {
