diff options
| -rw-r--r-- | .tx/config | 9 | ||||
| -rw-r--r-- | CONTRIBUTING.md | 54 | ||||
| -rw-r--r-- | docs/topics/internationalisation.md | 95 | ||||
| -rw-r--r-- | rest_framework/authentication.py | 17 | ||||
| -rw-r--r-- | rest_framework/authtoken/serializers.py | 2 | ||||
| -rw-r--r-- | rest_framework/exceptions.py | 34 | ||||
| -rw-r--r-- | rest_framework/fields.py | 41 | ||||
| -rw-r--r-- | rest_framework/generics.py | 12 | ||||
| -rw-r--r-- | rest_framework/locale/en_US/LC_MESSAGES/django.po | 316 | ||||
| -rw-r--r-- | rest_framework/relations.py | 6 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 2 | ||||
| -rw-r--r-- | rest_framework/versioning.py | 2 | ||||
| -rw-r--r-- | tests/test_fields.py | 38 | ||||
| -rw-r--r-- | tests/test_generics.py | 6 | ||||
| -rw-r--r-- | tests/test_relations.py | 2 | ||||
| -rw-r--r-- | tests/test_serializer_bulk_update.py | 4 | 
16 files changed, 557 insertions, 83 deletions
| diff --git a/.tx/config b/.tx/config new file mode 100644 index 00000000..271fa1e3 --- /dev/null +++ b/.tx/config @@ -0,0 +1,9 @@ +[main] +host = https://www.transifex.com + +[django-rest-framework.djangopo] +file_filter = rest_framework/locale/<lang>/LC_MESSAGES/django.po +source_file = rest_framework/locale/en_US/LC_MESSAGES/django.po +source_lang = en_US +type = PO + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b963a499..d94eb87e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -177,6 +177,57 @@ We recommend the [`django-reusable-app`][django-reusable-app] template as a good  Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation. +# Translations + +If REST framework isn't translated into your language you can request that it is at the [Transifex project][transifex]. + +## Managing Transfiex +The [official Transifex client][transifex-client] is used to upload and download translations to Transifex. The client is installed using pip: + +``` +pip install transifex-client +``` + +To use it you'll need a login to Transifex which has a password, and you'll need to have administrative access to the Transifex project. You'll need to create a `~/.transifexrc` file which contains your authentication information: + +``` +[https://www.transifex.com] +username = user +token = +password = p@ssw0rd +hostname = https://www.transifex.com +``` + +## Upload new source translations +When any user-visible strings are changed, they should be uploaded to Transifex so that the translators can start to translate them. To do this, just run: + +``` +cd rest_framework +django-admin.py makemessages -l en_US +cd .. +tx push -s +``` + +When pushing source files, Transifex will update the source strings of a resource to match those from the new source file. + +Here's how differences between the old and new source files will be handled: + +* New strings will be added. +* Modified strings will be added as well. +* Strings which do not exist in the new source file will be removed from the database, along with their translations. If that source strings gets re-added later then [Transifex Translation Memory][translation-memory] will automatically restore the translated string too.  + + +## Get translations +When a translator has finished translating their work needs to be downloaded from Transifex into the source repo. To do this, run: + +``` +tx pull -a +cd rest_framework +django-admin.py compilemessages +``` + +You can then commit as normal. +  [cite]: http://www.w3.org/People/Berners-Lee/FAQ.html  [code-of-conduct]: https://www.djangoproject.com/conduct/  [google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework @@ -190,3 +241,6 @@ Once your package is decently documented and available on PyPI open a pull reque  [docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs  [mou]: http://mouapp.com/  [django-reusable-app]: https://github.com/dabapps/django-reusable-app +[transifex]: https://www.transifex.com/projects/p/django-rest-framework/ +[transifex-client]: https://pypi.python.org/pypi/transifex-client +[translation-memory]: http://docs.transifex.com/guides/tm#let-tm-automatically-populate-translations
\ No newline at end of file diff --git a/docs/topics/internationalisation.md b/docs/topics/internationalisation.md new file mode 100644 index 00000000..2a476c86 --- /dev/null +++ b/docs/topics/internationalisation.md @@ -0,0 +1,95 @@ +# Internationalisation +REST framework ships with translatable error messages.  You can make these appear in your language enabling [Django's standard translation mechanisms][django-translation] and by translating the messages into your language. + +## How to translate REST Framework errors + +REST framework translations are managed online using [Transifex.com][transifex]. To get started, checkout the guide in the [CONTRIBUTING.md guide][contributing]. + +Sometimes you may want to use REST Framework in a language which has not been translated yet on Transifex. If that is the case then you should translate the error messages locally. + +#### How to translate REST Framework error messages locally: + +This guide assumes you are already familiar with how to translate a Django app.  If you're not, start by reading [Django's translation docs][django-translation]. + +1. Make a new folder where you want to store the translated errors. Add this  +path to your [`LOCALE_PATHS`][django-locale-paths] setting.  + +  --- + +  **Note:** For the rest of  +this document we will assume the path you created was  +`/home/www/project/conf/locale/`, and that you have updated your `settings.py` to include the setting: + +  ``` +  LOCALE_PATHS = ( +      '/home/www/project/conf/locale/', +  ) +  ``` + +  --- + +2. Now create a subfolder for the language you want to translate. The folder should be named using [locale  +name][django-locale-name] notation.  E.g. `de`, `pt_BR`, `es_AR`, etc. + +  ``` +  mkdir /home/www/project/conf/locale/pt_BR/LC_MESSAGES +  ``` + +3. Now copy the base translations file from the REST framework source code  +into your translations folder + +  ``` +  cp /home/user/.virtualenvs/myproject/lib/python2.7/site-packages/rest_framework/locale/en_US/LC_MESSAGES/django.po +  /home/www/project/conf/locale/pt_BR/LC_MESSAGES +  ``` +   +  This should create the file  +  `/home/www/project/conf/locale/pt_BR/LC_MESSAGES/django.po` +   +  --- + +  **Note:** To find out where `rest_framework` is installed, run  + +  ``` +  python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())" +  ``` + +  --- +   +   +4. Edit `/home/www/project/conf/locale/pt_BR/LC_MESSAGES/django.po` and  +translate all the error messages. + +5. Run `manage.py compilemessages -l pt_BR` to make the translations  +available for Django to use. You should see a message + +    ``` +    processing file django.po in /home/www/project/conf/locale/pt_BR/LC_MESSAGES +    ``` + +6. Restart your server. + + + +## How Django chooses which language to use +REST framework will use the same preferences to select which language to  +display as Django does.  You can find more info in the [Django docs on discovering language preferences][django-language-preference].  For reference, these are + +1. First, it looks for the language prefix in the requested URL +2. Failing that, it looks for the `LANGUAGE_SESSION_KEY` key in the current user’s session. +3. Failing that, it looks for a cookie +4. Failing that, it looks at the `Accept-Language` HTTP header. +5. Failing that, it uses the global `LANGUAGE_CODE` setting. + +--- + +**Note:** You'll need to include the `django.middleware.locale.LocaleMiddleware` to enable any of the per-request language preferences. + +--- + + +[django-translation]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation +[django-language-preference]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#how-django-discovers-language-preference +[django-locale-paths]: https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-LOCALE_PATHS +[django-locale-name]: https://docs.djangoproject.com/en/1.7/topics/i18n/#term-locale-name +[contributing]: ../../CONTRIBUTING.md diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 124ef68a..11db0585 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals  import base64  from django.contrib.auth import authenticate  from django.middleware.csrf import CsrfViewMiddleware +from django.utils.translation import ugettext_lazy as _  from rest_framework import exceptions, HTTP_HEADER_ENCODING  from rest_framework.authtoken.models import Token @@ -65,16 +66,16 @@ class BasicAuthentication(BaseAuthentication):              return None          if len(auth) == 1: -            msg = 'Invalid basic header. No credentials provided.' +            msg = _('Invalid basic header. No credentials provided.')              raise exceptions.AuthenticationFailed(msg)          elif len(auth) > 2: -            msg = 'Invalid basic header. Credentials string should not contain spaces.' +            msg = _('Invalid basic header. Credentials string should not contain spaces.')              raise exceptions.AuthenticationFailed(msg)          try:              auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')          except (TypeError, UnicodeDecodeError): -            msg = 'Invalid basic header. Credentials not correctly base64 encoded' +            msg = _('Invalid basic header. Credentials not correctly base64 encoded.')              raise exceptions.AuthenticationFailed(msg)          userid, password = auth_parts[0], auth_parts[2] @@ -86,7 +87,7 @@ class BasicAuthentication(BaseAuthentication):          """          user = authenticate(username=userid, password=password)          if user is None or not user.is_active: -            raise exceptions.AuthenticationFailed('Invalid username/password') +            raise exceptions.AuthenticationFailed(_('Invalid username/password.'))          return (user, None)      def authenticate_header(self, request): @@ -152,10 +153,10 @@ class TokenAuthentication(BaseAuthentication):              return None          if len(auth) == 1: -            msg = 'Invalid token header. No credentials provided.' +            msg = _('Invalid token header. No credentials provided.')              raise exceptions.AuthenticationFailed(msg)          elif len(auth) > 2: -            msg = 'Invalid token header. Token string should not contain spaces.' +            msg = _('Invalid token header. Token string should not contain spaces.')              raise exceptions.AuthenticationFailed(msg)          return self.authenticate_credentials(auth[1]) @@ -164,10 +165,10 @@ class TokenAuthentication(BaseAuthentication):          try:              token = self.model.objects.get(key=key)          except self.model.DoesNotExist: -            raise exceptions.AuthenticationFailed('Invalid token') +            raise exceptions.AuthenticationFailed(_('Invalid token.'))          if not token.user.is_active: -            raise exceptions.AuthenticationFailed('User inactive or deleted') +            raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))          return (token.user, token) diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py index f31dded1..37ade255 100644 --- a/rest_framework/authtoken/serializers.py +++ b/rest_framework/authtoken/serializers.py @@ -23,7 +23,7 @@ class AuthTokenSerializer(serializers.Serializer):                  msg = _('Unable to log in with provided credentials.')                  raise exceptions.ValidationError(msg)          else: -            msg = _('Must include "username" and "password"') +            msg = _('Must include "username" and "password".')              raise exceptions.ValidationError(msg)          attrs['user'] = user diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index bcfd8961..f954c13e 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -7,8 +7,7 @@ In addition Django's built in 403 and 404 exceptions are handled.  from __future__ import unicode_literals  from django.utils import six  from django.utils.encoding import force_text -from django.utils.translation import ugettext_lazy as _ -from django.utils.translation import ungettext_lazy +from django.utils.translation import ugettext_lazy as _, ungettext  from rest_framework import status  import math @@ -36,7 +35,7 @@ class APIException(Exception):      Subclasses should provide `.status_code` and `.default_detail` properties.      """      status_code = status.HTTP_500_INTERNAL_SERVER_ERROR -    default_detail = _('A server error occured') +    default_detail = _('A server error occurred.')      def __init__(self, detail=None):          if detail is not None: @@ -91,23 +90,23 @@ class PermissionDenied(APIException):  class NotFound(APIException):      status_code = status.HTTP_404_NOT_FOUND -    default_detail = _('Not found') +    default_detail = _('Not found.')  class MethodNotAllowed(APIException):      status_code = status.HTTP_405_METHOD_NOT_ALLOWED -    default_detail = _("Method '%s' not allowed.") +    default_detail = _('Method "{method}" not allowed.')      def __init__(self, method, detail=None):          if detail is not None:              self.detail = force_text(detail)          else: -            self.detail = force_text(self.default_detail) % method +            self.detail = force_text(self.default_detail).format(method=method)  class NotAcceptable(APIException):      status_code = status.HTTP_406_NOT_ACCEPTABLE -    default_detail = _('Could not satisfy the request Accept header') +    default_detail = _('Could not satisfy the request Accept header.')      def __init__(self, detail=None, available_renderers=None):          if detail is not None: @@ -119,23 +118,22 @@ class NotAcceptable(APIException):  class UnsupportedMediaType(APIException):      status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE -    default_detail = _("Unsupported media type '%s' in request.") +    default_detail = _('Unsupported media type "{media_type}" in request.')      def __init__(self, media_type, detail=None):          if detail is not None:              self.detail = force_text(detail)          else: -            self.detail = force_text(self.default_detail) % media_type +            self.detail = force_text(self.default_detail).format( +                media_type=media_type +            )  class Throttled(APIException):      status_code = status.HTTP_429_TOO_MANY_REQUESTS      default_detail = _('Request was throttled.') -    extra_detail = ungettext_lazy( -        'Expected available in %(wait)d second.', -        'Expected available in %(wait)d seconds.', -        'wait' -    ) +    extra_detail_singular = 'Expected available in {wait} second.' +    extra_detail_plural = 'Expected available in {wait} seconds.'      def __init__(self, wait=None, detail=None):          if detail is not None: @@ -147,6 +145,8 @@ class Throttled(APIException):              self.wait = None          else:              self.wait = math.ceil(wait) -            self.detail += ' ' + force_text( -                self.extra_detail % {'wait': self.wait} -            ) +            self.detail += ' ' + force_text(ungettext( +                self.extra_detail_singular.format(wait=self.wait), +                self.extra_detail_plural.format(wait=self.wait), +                self.wait +            )) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index cc9410aa..342564d3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -483,7 +483,7 @@ class Field(object):  class BooleanField(Field):      default_error_messages = { -        'invalid': _('`{input}` is not a valid boolean.') +        'invalid': _('"{input}" is not a valid boolean.')      }      default_empty_html = False      initial = False @@ -511,7 +511,7 @@ class BooleanField(Field):  class NullBooleanField(Field):      default_error_messages = { -        'invalid': _('`{input}` is not a valid boolean.') +        'invalid': _('"{input}" is not a valid boolean.')      }      initial = None      TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) @@ -611,7 +611,7 @@ class RegexField(CharField):  class SlugField(CharField):      default_error_messages = { -        'invalid': _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.") +        'invalid': _('Enter a valid "slug" consisting of letters, numbers, underscores or hyphens.')      }      def __init__(self, **kwargs): @@ -623,7 +623,7 @@ class SlugField(CharField):  class URLField(CharField):      default_error_messages = { -        'invalid': _("Enter a valid URL.") +        'invalid': _('Enter a valid URL.')      }      def __init__(self, **kwargs): @@ -639,7 +639,7 @@ class IntegerField(Field):          'invalid': _('A valid integer is required.'),          'max_value': _('Ensure this value is less than or equal to {max_value}.'),          'min_value': _('Ensure this value is greater than or equal to {min_value}.'), -        'max_string_length': _('String value too large') +        'max_string_length': _('String value too large.')      }      MAX_STRING_LENGTH = 1000  # Guard against malicious string inputs. @@ -670,10 +670,10 @@ class IntegerField(Field):  class FloatField(Field):      default_error_messages = { -        'invalid': _("A valid number is required."), +        'invalid': _('A valid number is required.'),          'max_value': _('Ensure this value is less than or equal to {max_value}.'),          'min_value': _('Ensure this value is greater than or equal to {min_value}.'), -        'max_string_length': _('String value too large') +        'max_string_length': _('String value too large.')      }      MAX_STRING_LENGTH = 1000  # Guard against malicious string inputs. @@ -709,7 +709,7 @@ class DecimalField(Field):          'max_digits': _('Ensure that there are no more than {max_digits} digits in total.'),          'max_decimal_places': _('Ensure that there are no more than {max_decimal_places} decimal places.'),          'max_whole_digits': _('Ensure that there are no more than {max_whole_digits} digits before the decimal point.'), -        'max_string_length': _('String value too large') +        'max_string_length': _('String value too large.')      }      MAX_STRING_LENGTH = 1000  # Guard against malicious string inputs. @@ -792,7 +792,7 @@ class DecimalField(Field):  class DateTimeField(Field):      default_error_messages = { -        'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}'), +        'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'),          'date': _('Expected a datetime but got a date.'),      }      format = api_settings.DATETIME_FORMAT @@ -857,7 +857,7 @@ class DateTimeField(Field):  class DateField(Field):      default_error_messages = { -        'invalid': _('Date has wrong format. Use one of these formats instead: {format}'), +        'invalid': _('Date has wrong format. Use one of these formats instead: {format}.'),          'datetime': _('Expected a date but got a datetime.'),      }      format = api_settings.DATE_FORMAT @@ -915,7 +915,7 @@ class DateField(Field):  class TimeField(Field):      default_error_messages = { -        'invalid': _('Time has wrong format. Use one of these formats instead: {format}'), +        'invalid': _('Time has wrong format. Use one of these formats instead: {format}.'),      }      format = api_settings.TIME_FORMAT      input_formats = api_settings.TIME_INPUT_FORMATS @@ -971,7 +971,7 @@ class TimeField(Field):  class ChoiceField(Field):      default_error_messages = { -        'invalid_choice': _('`{input}` is not a valid choice.') +        'invalid_choice': _('"{input}" is not a valid choice.')      }      def __init__(self, choices, **kwargs): @@ -1015,8 +1015,8 @@ class ChoiceField(Field):  class MultipleChoiceField(ChoiceField):      default_error_messages = { -        'invalid_choice': _('`{input}` is not a valid choice.'), -        'not_a_list': _('Expected a list of items but got type `{input_type}`.') +        'invalid_choice': _('"{input}" is not a valid choice.'), +        'not_a_list': _('Expected a list of items but got type "{input_type}".')      }      default_empty_html = [] @@ -1046,10 +1046,10 @@ class MultipleChoiceField(ChoiceField):  class FileField(Field):      default_error_messages = { -        'required': _("No file was submitted."), -        'invalid': _("The submitted data was not a file. Check the encoding type on the form."), -        'no_name': _("No filename could be determined."), -        'empty': _("The submitted file is empty."), +        'required': _('No file was submitted.'), +        'invalid': _('The submitted data was not a file. Check the encoding type on the form.'), +        'no_name': _('No filename could be determined.'), +        'empty': _('The submitted file is empty.'),          'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'),      }      use_url = api_settings.UPLOADED_FILES_USE_URL @@ -1092,8 +1092,7 @@ class FileField(Field):  class ImageField(FileField):      default_error_messages = {          'invalid_image': _( -            'Upload a valid image. The file you uploaded was either not an ' -            'image or a corrupted image.' +            'Upload a valid image. The file you uploaded was either not an image or a corrupted image.'          ),      } @@ -1118,7 +1117,7 @@ class ListField(Field):      child = None      initial = []      default_error_messages = { -        'not_a_list': _('Expected a list of items but got type `{input_type}`') +        'not_a_list': _('Expected a list of items but got type "{input_type}".')      }      def __init__(self, *args, **kwargs): diff --git a/rest_framework/generics.py b/rest_framework/generics.py index e6db155e..0d709c37 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -119,15 +119,15 @@ class GenericAPIView(views.APIView):              if page == 'last':                  page_number = paginator.num_pages              else: -                raise Http404(_("Page is not 'last', nor can it be converted to an int.")) +                raise Http404(_('Choose a valid page number. Page numbers must be a whole number, or must be the string "last".')) +          try:              page = paginator.page(page_number)          except InvalidPage as exc: -            error_format = _('Invalid page (%(page_number)s): %(message)s') -            raise Http404(error_format % { -                'page_number': page_number, -                'message': six.text_type(exc) -            }) +            error_format = _('Invalid page "{page_number}": {message}.') +            raise Http404(error_format.format( +                page_number=page_number, message=six.text_type(exc) +            ))          return page diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.po b/rest_framework/locale/en_US/LC_MESSAGES/django.po new file mode 100644 index 00000000..d98225ce --- /dev/null +++ b/rest_framework/locale/en_US/LC_MESSAGES/django.po @@ -0,0 +1,316 @@ +# 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. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-07 18:21+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\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" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "" + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "" + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "" + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "" + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "" + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "" + +#: authentication.py:168 +msgid "Invalid token." +msgstr "" + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "" + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "" + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "" + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "" + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "" + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "" + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "" + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "" + +#: exceptions.py:93 +msgid "Not found." +msgstr "" + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "" + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "" + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "" + +#: fields.py:152 relations.py:131 relations.py:155 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "" + +#: fields.py:153 +msgid "This field may not be null." +msgstr "" + +#: fields.py:480 fields.py:508 +msgid "\"{input}\" is not a valid boolean." +msgstr "" + +#: fields.py:543 +msgid "This field may not be blank." +msgstr "" + +#: fields.py:544 fields.py:1252 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "" + +#: fields.py:545 +msgid "Ensure this field has at least {min_length} characters." +msgstr "" + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "" + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "" + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "" + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "" + +#: fields.py:640 +msgid "A valid integer is required." +msgstr "" + +#: fields.py:641 fields.py:675 fields.py:708 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "" + +#: fields.py:642 fields.py:676 fields.py:709 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "" + +#: fields.py:643 fields.py:677 fields.py:713 +msgid "String value too large." +msgstr "" + +#: fields.py:674 fields.py:707 +msgid "A valid number is required." +msgstr "" + +#: fields.py:710 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "" + +#: fields.py:711 +msgid "Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "" + +#: fields.py:712 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "" + +#: fields.py:796 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:797 +msgid "Expected a datetime but got a date." +msgstr "" + +#: fields.py:861 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:862 +msgid "Expected a date but got a datetime." +msgstr "" + +#: fields.py:919 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:975 fields.py:1019 +msgid "\"{input}\" is not a valid choice." +msgstr "" + +#: fields.py:1020 fields.py:1121 serializers.py:476 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1050 +msgid "No file was submitted." +msgstr "" + +#: fields.py:1051 +msgid "The submitted data was not a file. Check the encoding type on the form." +msgstr "" + +#: fields.py:1052 +msgid "No filename could be determined." +msgstr "" + +#: fields.py:1053 +msgid "The submitted file is empty." +msgstr "" + +#: fields.py:1054 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "" + +#: fields.py:1096 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: generics.py:123 +msgid "" +"Choose a valid page number. Page numbers must be a whole number, or must be " +"the string \"last\"." +msgstr "" + +#: generics.py:128 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "" + +#: relations.py:132 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:133 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:156 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:157 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:158 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:159 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:294 +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:295 +msgid "Invalid value." +msgstr "" + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: validators.py:22 +msgid "This field must be unique." +msgstr "" + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "" diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 7b119291..05ac3d1c 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -129,7 +129,7 @@ class StringRelatedField(RelatedField):  class PrimaryKeyRelatedField(RelatedField):      default_error_messages = {          'required': _('This field is required.'), -        'does_not_exist': _("Invalid pk '{pk_value}' - object does not exist."), +        'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),          'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'),      } @@ -153,7 +153,7 @@ class HyperlinkedRelatedField(RelatedField):      default_error_messages = {          'required': _('This field is required.'), -        'no_match': _('Invalid hyperlink - No URL match'), +        'no_match': _('Invalid hyperlink - No URL match.'),          'incorrect_match': _('Invalid hyperlink - Incorrect URL match.'),          'does_not_exist': _('Invalid hyperlink - Object does not exist.'),          'incorrect_type': _('Incorrect type. Expected URL string, received {data_type}.'), @@ -291,7 +291,7 @@ class SlugRelatedField(RelatedField):      """      default_error_messages = { -        'does_not_exist': _("Object with {slug_name}={value} does not exist."), +        'does_not_exist': _('Object with {slug_name}={value} does not exist.'),          'invalid': _('Invalid value.'),      } diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d1bd3ec3..77d3f202 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -479,7 +479,7 @@ class ListSerializer(BaseSerializer):      many = True      default_error_messages = { -        'not_a_list': _('Expected a list of items but got type `{input_type}`.') +        'not_a_list': _('Expected a list of items but got type "{input_type}".')      }      def __init__(self, *args, **kwargs): diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py index 440efd13..e31c71e9 100644 --- a/rest_framework/versioning.py +++ b/rest_framework/versioning.py @@ -36,7 +36,7 @@ class AcceptHeaderVersioning(BaseVersioning):      Host: example.com      Accept: application/json; version=1.0      """ -    invalid_version_message = _("Invalid version in 'Accept' header.") +    invalid_version_message = _('Invalid version in "Accept" header.')      def determine_version(self, request, *args, **kwargs):          media_type = _MediaType(request.accepted_media_type) diff --git a/tests/test_fields.py b/tests/test_fields.py index 775d4618..dc21c234 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -346,7 +346,7 @@ class TestBooleanField(FieldValues):          False: False,      }      invalid_inputs = { -        'foo': ['`foo` is not a valid boolean.'], +        'foo': ['"foo" is not a valid boolean.'],          None: ['This field may not be null.']      }      outputs = { @@ -376,7 +376,7 @@ class TestNullBooleanField(FieldValues):          None: None      }      invalid_inputs = { -        'foo': ['`foo` is not a valid boolean.'], +        'foo': ['"foo" is not a valid boolean.'],      }      outputs = {          'true': True, @@ -447,7 +447,7 @@ class TestSlugField(FieldValues):          'slug-99': 'slug-99',      }      invalid_inputs = { -        'slug 99': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."] +        'slug 99': ['Enter a valid "slug" consisting of letters, numbers, underscores or hyphens.']      }      outputs = {}      field = serializers.SlugField() @@ -648,8 +648,8 @@ class TestDateField(FieldValues):          datetime.date(2001, 1, 1): datetime.date(2001, 1, 1),      }      invalid_inputs = { -        'abc': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'], -        '2001-99-99': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'], +        'abc': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]].'], +        '2001-99-99': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]].'],          datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],      }      outputs = { @@ -666,7 +666,7 @@ class TestCustomInputFormatDateField(FieldValues):          '1 Jan 2001': datetime.date(2001, 1, 1),      }      invalid_inputs = { -        '2001-01-01': ['Date has wrong format. Use one of these formats instead: DD [Jan-Dec] YYYY'] +        '2001-01-01': ['Date has wrong format. Use one of these formats instead: DD [Jan-Dec] YYYY.']      }      outputs = {}      field = serializers.DateField(input_formats=['%d %b %Y']) @@ -710,8 +710,8 @@ class TestDateTimeField(FieldValues):          '2001-01-01T14:00+01:00' if (django.VERSION > (1, 4)) else '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC())      }      invalid_inputs = { -        'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'], -        '2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'], +        'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'], +        '2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'],          datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],      }      outputs = { @@ -729,7 +729,7 @@ class TestCustomInputFormatDateTimeField(FieldValues):          '1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()),      }      invalid_inputs = { -        '2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY'] +        '2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY.']      }      outputs = {}      field = serializers.DateTimeField(default_timezone=timezone.UTC(), input_formats=['%I:%M%p, %d %b %Y']) @@ -781,8 +781,8 @@ class TestTimeField(FieldValues):          datetime.time(13, 00): datetime.time(13, 00),      }      invalid_inputs = { -        'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'], -        '99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'], +        'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].'], +        '99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].'],      }      outputs = {          datetime.time(13, 00): '13:00:00' @@ -798,7 +798,7 @@ class TestCustomInputFormatTimeField(FieldValues):          '1:00pm': datetime.time(13, 00),      }      invalid_inputs = { -        '13:00': ['Time has wrong format. Use one of these formats instead: hh:mm[AM|PM]'], +        '13:00': ['Time has wrong format. Use one of these formats instead: hh:mm[AM|PM].'],      }      outputs = {}      field = serializers.TimeField(input_formats=['%I:%M%p']) @@ -840,7 +840,7 @@ class TestChoiceField(FieldValues):          'good': 'good',      }      invalid_inputs = { -        'amazing': ['`amazing` is not a valid choice.'] +        'amazing': ['"amazing" is not a valid choice.']      }      outputs = {          'good': 'good', @@ -880,8 +880,8 @@ class TestChoiceFieldWithType(FieldValues):          3: 3,      }      invalid_inputs = { -        5: ['`5` is not a valid choice.'], -        'abc': ['`abc` is not a valid choice.'] +        5: ['"5" is not a valid choice.'], +        'abc': ['"abc" is not a valid choice.']      }      outputs = {          '1': 1, @@ -907,7 +907,7 @@ class TestChoiceFieldWithListChoices(FieldValues):          'good': 'good',      }      invalid_inputs = { -        'awful': ['`awful` is not a valid choice.'] +        'awful': ['"awful" is not a valid choice.']      }      outputs = {          'good': 'good' @@ -925,8 +925,8 @@ class TestMultipleChoiceField(FieldValues):          ('aircon', 'manual'): set(['aircon', 'manual']),      }      invalid_inputs = { -        'abc': ['Expected a list of items but got type `str`.'], -        ('aircon', 'incorrect'): ['`incorrect` is not a valid choice.'] +        'abc': ['Expected a list of items but got type "str".'], +        ('aircon', 'incorrect'): ['"incorrect" is not a valid choice.']      }      outputs = [          (['aircon', 'manual'], set(['aircon', 'manual'])) @@ -1036,7 +1036,7 @@ class TestListField(FieldValues):          (['1', '2', '3'], [1, 2, 3])      ]      invalid_inputs = [ -        ('not a list', ['Expected a list of items but got type `str`']), +        ('not a list', ['Expected a list of items but got type "str".']),          ([1, 2, 'error'], ['A valid integer is required.'])      ]      outputs = [ diff --git a/tests/test_generics.py b/tests/test_generics.py index 94023c30..fba8718f 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -117,7 +117,7 @@ class TestRootView(TestCase):          with self.assertNumQueries(0):              response = self.view(request).render()          self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) -        self.assertEqual(response.data, {"detail": "Method 'PUT' not allowed."}) +        self.assertEqual(response.data, {"detail": 'Method "PUT" not allowed.'})      def test_delete_root_view(self):          """ @@ -127,7 +127,7 @@ class TestRootView(TestCase):          with self.assertNumQueries(0):              response = self.view(request).render()          self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) -        self.assertEqual(response.data, {"detail": "Method 'DELETE' not allowed."}) +        self.assertEqual(response.data, {"detail": 'Method "DELETE" not allowed.'})      def test_post_cannot_set_id(self):          """ @@ -181,7 +181,7 @@ class TestInstanceView(TestCase):          with self.assertNumQueries(0):              response = self.view(request).render()          self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) -        self.assertEqual(response.data, {"detail": "Method 'POST' not allowed."}) +        self.assertEqual(response.data, {"detail": 'Method "POST" not allowed.'})      def test_put_instance_view(self):          """ diff --git a/tests/test_relations.py b/tests/test_relations.py index 62353dc2..08c92242 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -33,7 +33,7 @@ class TestPrimaryKeyRelatedField(APISimpleTestCase):          with pytest.raises(serializers.ValidationError) as excinfo:              self.field.to_internal_value(4)          msg = excinfo.value.detail[0] -        assert msg == "Invalid pk '4' - object does not exist." +        assert msg == 'Invalid pk "4" - object does not exist.'      def test_pk_related_lookup_invalid_type(self):          with pytest.raises(serializers.ValidationError) as excinfo: diff --git a/tests/test_serializer_bulk_update.py b/tests/test_serializer_bulk_update.py index fb881a75..bc955b2e 100644 --- a/tests/test_serializer_bulk_update.py +++ b/tests/test_serializer_bulk_update.py @@ -101,7 +101,7 @@ class BulkCreateSerializerTests(TestCase):          serializer = self.BookSerializer(data=data, many=True)          self.assertEqual(serializer.is_valid(), False) -        expected_errors = {'non_field_errors': ['Expected a list of items but got type `int`.']} +        expected_errors = {'non_field_errors': ['Expected a list of items but got type "int".']}          self.assertEqual(serializer.errors, expected_errors) @@ -118,6 +118,6 @@ class BulkCreateSerializerTests(TestCase):          serializer = self.BookSerializer(data=data, many=True)          self.assertEqual(serializer.is_valid(), False) -        expected_errors = {'non_field_errors': ['Expected a list of items but got type `dict`.']} +        expected_errors = {'non_field_errors': ['Expected a list of items but got type "dict".']}          self.assertEqual(serializer.errors, expected_errors) | 
