Fixed #11585 -- Added ability to translate and prefix URL patterns with a language code as an alternative method for language discovery. Many thanks to Orne Brocaar for his initial work and Carl Meyer for feedback.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16405 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jannis Leidel 2011-06-15 17:29:10 +00:00
parent 62bb4b8c37
commit 896e3c69c7
29 changed files with 750 additions and 57 deletions

View File

@ -94,6 +94,7 @@ answer newbie questions, and generally made Django that much better:
Sean Brant Sean Brant
Andrew Brehaut <http://brehaut.net/blog> Andrew Brehaut <http://brehaut.net/blog>
David Brenneman <http://davidbrenneman.com> David Brenneman <http://davidbrenneman.com>
Orne Brocaar <http://brocaar.com/>
brut.alll@gmail.com brut.alll@gmail.com
bthomas bthomas
btoll@bestweb.net btoll@bestweb.net

View File

@ -1,5 +1,8 @@
from django.core.urlresolvers import RegexURLPattern, RegexURLResolver from django.core.urlresolvers import (RegexURLPattern,
RegexURLResolver, LocaleRegexURLResolver)
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils.importlib import import_module
__all__ = ['handler404', 'handler500', 'include', 'patterns', 'url'] __all__ = ['handler404', 'handler500', 'include', 'patterns', 'url']
@ -15,6 +18,21 @@ def include(arg, namespace=None, app_name=None):
else: else:
# No namespace hint - use manually provided namespace # No namespace hint - use manually provided namespace
urlconf_module = arg urlconf_module = arg
if isinstance(urlconf_module, basestring):
urlconf_module = import_module(urlconf_module)
patterns = getattr(urlconf_module, 'urlpatterns', urlconf_module)
# Make sure we can iterate through the patterns (without this, some
# testcases will break).
if isinstance(patterns, (list, tuple)):
for url_pattern in patterns:
# Test if the LocaleRegexURLResolver is used within the include;
# this should throw an error since this is not allowed!
if isinstance(url_pattern, LocaleRegexURLResolver):
raise ImproperlyConfigured(
'Using i18n_patterns in an included URLconf is not allowed.')
return (urlconf_module, app_name, namespace) return (urlconf_module, app_name, namespace)
def patterns(prefix, *args): def patterns(prefix, *args):

View File

@ -1,4 +1,19 @@
from django.conf.urls.defaults import * from django.conf import settings
from django.conf.urls.defaults import patterns
from django.core.urlresolvers import LocaleRegexURLResolver
def i18n_patterns(prefix, *args):
"""
Adds the language code prefix to every URL pattern within this
function. This may only be used in the root URLconf, not in an included
URLconf.
"""
pattern_list = patterns(prefix, *args)
if not settings.USE_I18N:
return pattern_list
return [LocaleRegexURLResolver(pattern_list)]
urlpatterns = patterns('', urlpatterns = patterns('',
(r'^setlang/$', 'django.views.i18n.set_language'), (r'^setlang/$', 'django.views.i18n.set_language'),

View File

@ -346,12 +346,12 @@ def extract_views_from_urlpatterns(urlpatterns, base=''):
""" """
views = [] views = []
for p in urlpatterns: for p in urlpatterns:
if hasattr(p, '_get_callback'): if hasattr(p, 'callback'):
try: try:
views.append((p._get_callback(), base + p.regex.pattern)) views.append((p.callback, base + p.regex.pattern))
except ViewDoesNotExist: except ViewDoesNotExist:
continue continue
elif hasattr(p, '_get_url_patterns'): elif hasattr(p, 'url_patterns'):
try: try:
patterns = p.url_patterns patterns = p.url_patterns
except ImportError: except ImportError:

View File

@ -11,13 +11,14 @@ import re
from threading import local from threading import local
from django.http import Http404 from django.http import Http404
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.encoding import iri_to_uri, force_unicode, smart_str from django.utils.encoding import iri_to_uri, force_unicode, smart_str
from django.utils.functional import memoize, lazy from django.utils.functional import memoize, lazy
from django.utils.importlib import import_module from django.utils.importlib import import_module
from django.utils.regex_helper import normalize from django.utils.regex_helper import normalize
from django.utils.translation import get_language
_resolver_cache = {} # Maps URLconf modules to RegexURLResolver instances. _resolver_cache = {} # Maps URLconf modules to RegexURLResolver instances.
_callable_cache = {} # Maps view and url pattern names to their view functions. _callable_cache = {} # Maps view and url pattern names to their view functions.
@ -50,13 +51,13 @@ class ResolverMatch(object):
url_name = '.'.join([func.__module__, func.__name__]) url_name = '.'.join([func.__module__, func.__name__])
self.url_name = url_name self.url_name = url_name
@property
def namespace(self): def namespace(self):
return ':'.join(self.namespaces) return ':'.join(self.namespaces)
namespace = property(namespace)
@property
def view_name(self): def view_name(self):
return ':'.join([ x for x in [ self.namespace, self.url_name ] if x ]) return ':'.join([ x for x in [ self.namespace, self.url_name ] if x ])
view_name = property(view_name)
def __getitem__(self, index): def __getitem__(self, index):
return (self.func, self.args, self.kwargs)[index] return (self.func, self.args, self.kwargs)[index]
@ -115,13 +116,43 @@ def get_mod_func(callback):
return callback, '' return callback, ''
return callback[:dot], callback[dot+1:] return callback[:dot], callback[dot+1:]
class RegexURLPattern(object): class LocaleRegexProvider(object):
"""
A mixin to provide a default regex property which can vary by active
language.
"""
def __init__(self, regex):
# regex is either a string representing a regular expression, or a
# translatable string (using ugettext_lazy) representing a regular
# expression.
self._regex = regex
self._regex_dict = {}
@property
def regex(self):
"""
Returns a compiled regular expression, depending upon the activated
language-code.
"""
language_code = get_language()
if language_code not in self._regex_dict:
if isinstance(self._regex, basestring):
compiled_regex = re.compile(self._regex, re.UNICODE)
else:
regex = force_unicode(self._regex)
compiled_regex = re.compile(regex, re.UNICODE)
self._regex_dict[language_code] = compiled_regex
return self._regex_dict[language_code]
class RegexURLPattern(LocaleRegexProvider):
def __init__(self, regex, callback, default_args=None, name=None): def __init__(self, regex, callback, default_args=None, name=None):
# regex is a string representing a regular expression. LocaleRegexProvider.__init__(self, regex)
# callback is either a string like 'foo.views.news.stories.story_detail' # callback is either a string like 'foo.views.news.stories.story_detail'
# which represents the path to a module and a view function name, or a # which represents the path to a module and a view function name, or a
# callable object (view). # callable object (view).
self.regex = re.compile(regex, re.UNICODE)
if callable(callback): if callable(callback):
self._callback = callback self._callback = callback
else: else:
@ -157,7 +188,8 @@ class RegexURLPattern(object):
return ResolverMatch(self.callback, args, kwargs, self.name) return ResolverMatch(self.callback, args, kwargs, self.name)
def _get_callback(self): @property
def callback(self):
if self._callback is not None: if self._callback is not None:
return self._callback return self._callback
try: try:
@ -169,13 +201,11 @@ class RegexURLPattern(object):
mod_name, func_name = get_mod_func(self._callback_str) mod_name, func_name = get_mod_func(self._callback_str)
raise ViewDoesNotExist("Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e))) raise ViewDoesNotExist("Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e)))
return self._callback return self._callback
callback = property(_get_callback)
class RegexURLResolver(object): class RegexURLResolver(LocaleRegexProvider):
def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None): def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
# regex is a string representing a regular expression. LocaleRegexProvider.__init__(self, regex)
# urlconf_name is a string representing the module containing URLconfs. # urlconf_name is a string representing the module containing URLconfs.
self.regex = re.compile(regex, re.UNICODE)
self.urlconf_name = urlconf_name self.urlconf_name = urlconf_name
if not isinstance(urlconf_name, basestring): if not isinstance(urlconf_name, basestring):
self._urlconf_module = self.urlconf_name self._urlconf_module = self.urlconf_name
@ -183,9 +213,9 @@ class RegexURLResolver(object):
self.default_kwargs = default_kwargs or {} self.default_kwargs = default_kwargs or {}
self.namespace = namespace self.namespace = namespace
self.app_name = app_name self.app_name = app_name
self._reverse_dict = None self._reverse_dict = {}
self._namespace_dict = None self._namespace_dict = {}
self._app_dict = None self._app_dict = {}
def __repr__(self): def __repr__(self):
return smart_str(u'<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern)) return smart_str(u'<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern))
@ -194,6 +224,7 @@ class RegexURLResolver(object):
lookups = MultiValueDict() lookups = MultiValueDict()
namespaces = {} namespaces = {}
apps = {} apps = {}
language_code = get_language()
for pattern in reversed(self.url_patterns): for pattern in reversed(self.url_patterns):
p_pattern = pattern.regex.pattern p_pattern = pattern.regex.pattern
if p_pattern.startswith('^'): if p_pattern.startswith('^'):
@ -220,27 +251,30 @@ class RegexURLResolver(object):
lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args)) lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args))
if pattern.name is not None: if pattern.name is not None:
lookups.appendlist(pattern.name, (bits, p_pattern, pattern.default_args)) lookups.appendlist(pattern.name, (bits, p_pattern, pattern.default_args))
self._reverse_dict = lookups self._reverse_dict[language_code] = lookups
self._namespace_dict = namespaces self._namespace_dict[language_code] = namespaces
self._app_dict = apps self._app_dict[language_code] = apps
def _get_reverse_dict(self): @property
if self._reverse_dict is None: def reverse_dict(self):
language_code = get_language()
if language_code not in self._reverse_dict:
self._populate() self._populate()
return self._reverse_dict return self._reverse_dict[language_code]
reverse_dict = property(_get_reverse_dict)
def _get_namespace_dict(self): @property
if self._namespace_dict is None: def namespace_dict(self):
language_code = get_language()
if language_code not in self._namespace_dict:
self._populate() self._populate()
return self._namespace_dict return self._namespace_dict[language_code]
namespace_dict = property(_get_namespace_dict)
def _get_app_dict(self): @property
if self._app_dict is None: def app_dict(self):
language_code = get_language()
if language_code not in self._app_dict:
self._populate() self._populate()
return self._app_dict return self._app_dict[language_code]
app_dict = property(_get_app_dict)
def resolve(self, path): def resolve(self, path):
tried = [] tried = []
@ -267,22 +301,22 @@ class RegexURLResolver(object):
raise Resolver404({'tried': tried, 'path': new_path}) raise Resolver404({'tried': tried, 'path': new_path})
raise Resolver404({'path' : path}) raise Resolver404({'path' : path})
def _get_urlconf_module(self): @property
def urlconf_module(self):
try: try:
return self._urlconf_module return self._urlconf_module
except AttributeError: except AttributeError:
self._urlconf_module = import_module(self.urlconf_name) self._urlconf_module = import_module(self.urlconf_name)
return self._urlconf_module return self._urlconf_module
urlconf_module = property(_get_urlconf_module)
def _get_url_patterns(self): @property
def url_patterns(self):
patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module) patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
try: try:
iter(patterns) iter(patterns)
except TypeError: except TypeError:
raise ImproperlyConfigured("The included urlconf %s doesn't have any patterns in it" % self.urlconf_name) raise ImproperlyConfigured("The included urlconf %s doesn't have any patterns in it" % self.urlconf_name)
return patterns return patterns
url_patterns = property(_get_url_patterns)
def _resolve_special(self, view_type): def _resolve_special(self, view_type):
callback = getattr(self.urlconf_module, 'handler%s' % view_type, None) callback = getattr(self.urlconf_module, 'handler%s' % view_type, None)
@ -343,6 +377,25 @@ class RegexURLResolver(object):
raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword " raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
"arguments '%s' not found." % (lookup_view_s, args, kwargs)) "arguments '%s' not found." % (lookup_view_s, args, kwargs))
class LocaleRegexURLResolver(RegexURLResolver):
"""
A URL resolver that always matches the active language code as URL prefix.
Rather than taking a regex argument, we just override the ``regex``
function to always return the active language-code as regex.
"""
def __init__(self, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
super(LocaleRegexURLResolver, self).__init__(
None, urlconf_name, default_kwargs, app_name, namespace)
@property
def regex(self):
language_code = get_language()
if language_code not in self._regex_dict:
regex_compiled = re.compile('^%s/' % language_code, re.UNICODE)
self._regex_dict[language_code] = regex_compiled
return self._regex_dict[language_code]
def resolve(path, urlconf=None): def resolve(path, urlconf=None):
if urlconf is None: if urlconf is None:
urlconf = get_urlconf() urlconf = get_urlconf()

View File

@ -1,5 +1,7 @@
"this is the locale selecting middleware that will look at accept headers" "This is the locale selecting middleware that will look at accept headers"
from django.core.urlresolvers import get_resolver, LocaleRegexURLResolver
from django.http import HttpResponseRedirect
from django.utils.cache import patch_vary_headers from django.utils.cache import patch_vary_headers
from django.utils import translation from django.utils import translation
@ -18,8 +20,26 @@ class LocaleMiddleware(object):
request.LANGUAGE_CODE = translation.get_language() request.LANGUAGE_CODE = translation.get_language()
def process_response(self, request, response): def process_response(self, request, response):
language = translation.get_language()
translation.deactivate()
if (response.status_code == 404 and
not translation.get_language_from_path(request.path_info)
and self.is_language_prefix_patterns_used()):
return HttpResponseRedirect(
'/%s%s' % (language, request.get_full_path()))
patch_vary_headers(response, ('Accept-Language',)) patch_vary_headers(response, ('Accept-Language',))
if 'Content-Language' not in response: if 'Content-Language' not in response:
response['Content-Language'] = translation.get_language() response['Content-Language'] = language
translation.deactivate()
return response return response
def is_language_prefix_patterns_used(self):
"""
Returns `True` if the `LocaleRegexURLResolver` is used
at root level of the urlpatterns, else it returns `False`.
"""
for url_pattern in get_resolver(None).url_patterns:
if isinstance(url_pattern, LocaleRegexURLResolver):
return True
return False

View File

@ -144,6 +144,9 @@ def to_locale(language):
def get_language_from_request(request): def get_language_from_request(request):
return _trans.get_language_from_request(request) return _trans.get_language_from_request(request)
def get_language_from_path(path):
return _trans.get_language_from_path(path)
def templatize(src, origin=None): def templatize(src, origin=None):
return _trans.templatize(src, origin) return _trans.templatize(src, origin)

View File

@ -58,3 +58,7 @@ def to_locale(language):
def get_language_from_request(request): def get_language_from_request(request):
return settings.LANGUAGE_CODE return settings.LANGUAGE_CODE
def get_language_from_path(request):
return None

View File

@ -35,6 +35,8 @@ accept_language_re = re.compile(r'''
(?:\s*,\s*|$) # Multiple accepts per header. (?:\s*,\s*|$) # Multiple accepts per header.
''', re.VERBOSE) ''', re.VERBOSE)
language_code_prefix_re = re.compile(r'^/([\w-]+)/')
def to_locale(language, to_lower=False): def to_locale(language, to_lower=False):
""" """
Turns a language name (en-us) into a locale name (en_US). If 'to_lower' is Turns a language name (en-us) into a locale name (en_US). If 'to_lower' is
@ -336,14 +338,28 @@ def check_for_language(lang_code):
""" """
Checks whether there is a global language file for the given language Checks whether there is a global language file for the given language
code. This is used to decide whether a user-provided language is code. This is used to decide whether a user-provided language is
available. This is only used for language codes from either the cookies or available. This is only used for language codes from either the cookies
session and during format localization. or session and during format localization.
""" """
for path in all_locale_paths(): for path in all_locale_paths():
if gettext_module.find('django', path, [to_locale(lang_code)]) is not None: if gettext_module.find('django', path, [to_locale(lang_code)]) is not None:
return True return True
return False return False
def get_language_from_path(path, supported=None):
"""
Returns the language-code if there is a valid language-code
found in the `path`.
"""
if supported is None:
from django.conf import settings
supported = dict(settings.LANGUAGES)
regex_match = language_code_prefix_re.match(path)
if regex_match:
lang_code = regex_match.group(1)
if lang_code in supported and check_for_language(lang_code):
return lang_code
def get_language_from_request(request): def get_language_from_request(request):
""" """
Analyzes the request to find what language the user wants the system to Analyzes the request to find what language the user wants the system to
@ -355,6 +371,10 @@ def get_language_from_request(request):
from django.conf import settings from django.conf import settings
supported = dict(settings.LANGUAGES) supported = dict(settings.LANGUAGES)
lang_code = get_language_from_path(request.path_info, supported)
if lang_code is not None:
return lang_code
if hasattr(request, 'session'): if hasattr(request, 'session'):
lang_code = request.session.get('django_language', None) lang_code = request.session.get('django_language', None)
if lang_code in supported and lang_code is not None and check_for_language(lang_code): if lang_code in supported and lang_code is not None and check_for_language(lang_code):

View File

@ -167,6 +167,16 @@ a :class:`~django.forms.fields.GenericIPAddressField` form field and
the validators :data:`~django.core.validators.validate_ipv46_address` and the validators :data:`~django.core.validators.validate_ipv46_address` and
:data:`~django.core.validators.validate_ipv6_address` :data:`~django.core.validators.validate_ipv6_address`
Translating URL patterns
~~~~~~~~~~~~~~~~~~~~~~~~
Django 1.4 gained the ability to look for a language prefix in the URL pattern
when using the new :func:`django.conf.urls.i18n.i18n_patterns` helper function.
Additionally, it's now possible to define translatable URL patterns using
:func:`~django.utils.translation.ugettext_lazy`. See
:ref:`url-internationalization` for more information about the language prefix
and how to internationalize URL patterns.
Minor features Minor features
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~

View File

@ -59,7 +59,9 @@ matters, you should follow these guidelines:
* Make sure it's one of the first middlewares installed. * Make sure it's one of the first middlewares installed.
* It should come after ``SessionMiddleware``, because ``LocaleMiddleware`` * It should come after ``SessionMiddleware``, because ``LocaleMiddleware``
makes use of session data. makes use of session data. And it should come before ``CommonMiddleware``
because ``CommonMiddleware`` needs an activated language in order
to resolve the requested URL.
* If you use ``CacheMiddleware``, put ``LocaleMiddleware`` after it. * If you use ``CacheMiddleware``, put ``LocaleMiddleware`` after it.
For example, your :setting:`MIDDLEWARE_CLASSES` might look like this:: For example, your :setting:`MIDDLEWARE_CLASSES` might look like this::
@ -76,8 +78,15 @@ For example, your :setting:`MIDDLEWARE_CLASSES` might look like this::
``LocaleMiddleware`` tries to determine the user's language preference by ``LocaleMiddleware`` tries to determine the user's language preference by
following this algorithm: following this algorithm:
* First, it looks for a ``django_language`` key in the current user's .. versionchanged:: 1.4
session.
* First, it looks for the language prefix in the requested URL. This is
only performed when you are using the ``i18n_patterns`` function in your
root URLconf. See :ref:`url-internationalization` for more information
about the language prefix and how to internationalize URL patterns.
* Failing that, it looks for a ``django_language`` key in the current
user's session.
* Failing that, it looks for a cookie. * Failing that, it looks for a cookie.

View File

@ -753,6 +753,138 @@ This isn't as fast as string interpolation in Python, so keep it to those
cases where you really need it (for example, in conjunction with ``ngettext`` cases where you really need it (for example, in conjunction with ``ngettext``
to produce proper pluralizations). to produce proper pluralizations).
.. _url-internationalization:
Specifying translation strings: In URL patterns
===============================================
.. versionadded:: 1.4
.. module:: django.conf.urls.i18n
Django provides two mechanisms to internationalize URL patterns:
* Adding the language prefix to the root of the URL patterns to make it
possible for :class:`~django.middleware.locale.LocaleMiddleware` to detect
the language to activate from the requested URL.
* Making URL patterns themselves translatable via the
:func:`django.utils.translation.ugettext_lazy()` function.
.. warning::
Using either one of these features requires that an active language be set
for each request; in other words, you need to have
:class:`django.middleware.locale.LocaleMiddleware` in your
:setting:`MIDDLEWARE_CLASSES` setting.
Language prefix in URL patterns
-------------------------------
.. function:: i18n_patterns(prefix, pattern_description, ...)
This function can be used in your root URLconf as a replacement for the normal
:func:`django.conf.urls.defaults.patterns` function. Django will automatically
prepend the current active language code to all url patterns defined within
:func:`~django.conf.urls.i18n.i18n_patterns`. Example URL patterns::
from django.conf.urls.defaults import patterns, include, url
from django.conf.urls.i18n import i18n_patterns
urlpatterns = patterns(''
url(r'^sitemap\.xml$', 'sitemap.view', name='sitemap_xml'),
)
news_patterns = patterns(''
url(r'^$', 'news.views.index', name='index'),
url(r'^category/(?P<slug>[\w-]+)/$', 'news.views.category', name='category'),
url(r'^(?P<slug>[\w-]+)/$', 'news.views.details', name='detail'),
)
urlpatterns += i18n_patterns('',
url(r'^about/$', 'about.view', name='about'),
url(r'^news/$', include(news_patterns, namespace='news')),
)
After defining these URL patterns, Django will automatically add the
language prefix to the URL patterns that were added by the ``i18n_patterns``
function. Example::
from django.core.urlresolvers import reverse
from django.utils.translation import activate
>>> activate('en')
>>> reverse('sitemap_xml')
'/sitemap.xml'
>>> reverse('news:index')
'/en/news/'
>>> activate('nl')
>>> reverse('news:detail', kwargs={'slug': 'news-slug'})
'/nl/news/news-slug/'
.. warning::
:func:`~django.conf.urls.i18n.i18n_patterns` is only allowed in your root
URLconf. Using it within an included URLconf will throw an
:exc:`ImproperlyConfigured` exception.
.. warning::
Ensure that you don't have non-prefixed URL patterns that might collide
with an automatically-added language prefix.
Translating URL patterns
------------------------
URL patterns can also be marked translatable using the
:func:`~django.utils.translation.ugettext_lazy` function. Example::
from django.conf.urls.defaults import patterns, include, url
from django.conf.urls.i18n import i18n_patterns
from django.utils.translation import ugettext_lazy as _
urlpatterns = patterns(''
url(r'^sitemap\.xml$', 'sitemap.view', name='sitemap_xml'),
)
news_patterns = patterns(''
url(r'^$', 'news.views.index', name='index'),
url(_(r'^category/(?P<slug>[\w-]+)/$'), 'news.views.category', name='category'),
url(r'^(?P<slug>[\w-]+)/$', 'news.views.details', name='detail'),
)
urlpatterns += i18n_patterns('',
url(_(r'^about/$'), 'about.view', name='about'),
url(_(r'^news/$'), include(news_patterns, namespace='news')),
)
After you've created the translations (see :doc:`localization` for more
information), the :func:`~django.core.urlresolvers.reverse` function will
return the URL in the active language. Example::
from django.core.urlresolvers import reverse
from django.utils.translation import activate
>>> activate('en')
>>> reverse('news:category', kwargs={'slug': 'recent'})
'/en/news/category/recent/'
>>> activate('nl')
>>> reverse('news:category', kwargs={'slug': 'recent'})
'/nl/nieuws/categorie/recent/'
.. warning::
In most cases, it's best to use translated URLs only within a
language-code-prefixed block of patterns (using
:func:`~django.conf.urls.i18n.i18n_patterns`), to avoid the possibility
that a carelessly translated URL causes a collision with a non-translated
URL pattern.
.. _set_language-redirect-view: .. _set_language-redirect-view:
The ``set_language`` redirect view The ``set_language`` redirect view

View File

@ -0,0 +1,37 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-06-15 11:33+0200\n"
"PO-Revision-Date: 2011-06-14 16:16+0100\n"
"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: urls/default.py:11
msgid "^translated/$"
msgstr "^translated/$"
#: urls/default.py:12
msgid "^translated/(?P<slug>[\\w-]+)/$"
msgstr "^translated/(?P<slug>[\\w-]+)/$"
#: urls/default.py:17
msgid "^users/$"
msgstr "^users/$"
#: urls/default.py:18 urls/wrong.py:7
msgid "^account/"
msgstr "^account/"
#: urls/namespace.py:9 urls/wrong_namespace.py:10
msgid "^register/$"
msgstr "^register/$"

View File

@ -0,0 +1,38 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-06-15 11:33+0200\n"
"PO-Revision-Date: 2011-06-14 16:16+0100\n"
"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
#: urls/default.py:11
msgid "^translated/$"
msgstr "^vertaald/$"
#: urls/default.py:12
msgid "^translated/(?P<slug>[\\w-]+)/$"
msgstr "^vertaald/(?P<slug>[\\w-]+)/$"
#: urls/default.py:17
msgid "^users/$"
msgstr "^gebruikers/$"
#: urls/default.py:18 urls/wrong.py:7
msgid "^account/"
msgstr "^profiel/"
#: urls/namespace.py:9 urls/wrong_namespace.py:10
msgid "^register/$"
msgstr "^registeren/$"

View File

@ -0,0 +1,38 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-06-15 11:34+0200\n"
"PO-Revision-Date: 2011-06-14 16:17+0100\n"
"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
#: urls/default.py:11
msgid "^translated/$"
msgstr "^traduzidos/$"
#: urls/default.py:12
msgid "^translated/(?P<slug>[\\w-]+)/$"
msgstr "^traduzidos/(?P<slug>[\\w-]+)/$"
#: urls/default.py:17
msgid "^users/$"
msgstr "^usuarios/$"
#: urls/default.py:18 urls/wrong.py:7
msgid "^account/"
msgstr "^conta/"
#: urls/namespace.py:9 urls/wrong_namespace.py:10
msgid "^register/$"
msgstr "^registre-se/$"

View File

@ -0,0 +1,241 @@
import os
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse, clear_url_caches
from django.test import TestCase
from django.test.utils import override_settings
from django.utils import translation
class URLTestCaseBase(TestCase):
"""
TestCase base-class for the URL tests.
"""
urls = 'regressiontests.i18n.patterns.urls.default'
def setUp(self):
# Make sure the cache is empty before we are doing our tests.
clear_url_caches()
def tearDown(self):
# Make sure we will leave an empty cache for other testcases.
clear_url_caches()
URLTestCaseBase = override_settings(
USE_I18N=True,
LOCALE_PATHS=(
os.path.join(os.path.dirname(__file__), 'locale'),
),
TEMPLATE_DIRS=(
os.path.join(os.path.dirname(__file__), 'templates'),
),
LANGUAGE_CODE='en',
LANGUAGES=(
('nl', 'Dutch'),
('en', 'English'),
('pt-br', 'Brazilian Portuguese'),
),
MIDDLEWARE_CLASSES=(
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
),
)(URLTestCaseBase)
class URLPrefixTests(URLTestCaseBase):
"""
Tests if the `i18n_patterns` is adding the prefix correctly.
"""
def test_not_prefixed(self):
with translation.override('en'):
self.assertEqual(reverse('not-prefixed'), '/not-prefixed/')
with translation.override('nl'):
self.assertEqual(reverse('not-prefixed'), '/not-prefixed/')
def test_prefixed(self):
with translation.override('en'):
self.assertEqual(reverse('prefixed'), '/en/prefixed/')
with translation.override('nl'):
self.assertEqual(reverse('prefixed'), '/nl/prefixed/')
@override_settings(ROOT_URLCONF='regressiontests.i18n.patterns.urls.wrong')
def test_invalid_prefix_use(self):
self.assertRaises(ImproperlyConfigured, lambda: reverse('account:register'))
class URLDisabledTests(URLTestCaseBase):
urls = 'regressiontests.i18n.patterns.urls.disabled'
@override_settings(USE_I18N=False)
def test_prefixed_i18n_disabled(self):
with translation.override('en'):
self.assertEqual(reverse('prefixed'), '/prefixed/')
with translation.override('nl'):
self.assertEqual(reverse('prefixed'), '/prefixed/')
class URLTranslationTests(URLTestCaseBase):
"""
Tests if the pattern-strings are translated correctly (within the
`i18n_patterns` and the normal `patterns` function).
"""
def test_no_prefix_translated(self):
with translation.override('en'):
self.assertEqual(reverse('no-prefix-translated'), '/translated/')
self.assertEqual(reverse('no-prefix-translated-slug', kwargs={'slug': 'yeah'}), '/translated/yeah/')
with translation.override('nl'):
self.assertEqual(reverse('no-prefix-translated'), '/vertaald/')
self.assertEqual(reverse('no-prefix-translated-slug', kwargs={'slug': 'yeah'}), '/vertaald/yeah/')
with translation.override('pt-br'):
self.assertEqual(reverse('no-prefix-translated'), '/traduzidos/')
self.assertEqual(reverse('no-prefix-translated-slug', kwargs={'slug': 'yeah'}), '/traduzidos/yeah/')
def test_users_url(self):
with translation.override('en'):
self.assertEqual(reverse('users'), '/en/users/')
with translation.override('nl'):
self.assertEqual(reverse('users'), '/nl/gebruikers/')
with translation.override('pt-br'):
self.assertEqual(reverse('users'), '/pt-br/usuarios/')
class URLNamespaceTests(URLTestCaseBase):
"""
Tests if the translations are still working within namespaces.
"""
def test_account_register(self):
with translation.override('en'):
self.assertEqual(reverse('account:register'), '/en/account/register/')
with translation.override('nl'):
self.assertEqual(reverse('account:register'), '/nl/profiel/registeren/')
class URLRedirectTests(URLTestCaseBase):
"""
Tests if the user gets redirected to the right URL when there is no
language-prefix in the request URL.
"""
def test_no_prefix_response(self):
response = self.client.get('/not-prefixed/')
self.assertEqual(response.status_code, 200)
def test_en_redirect(self):
response = self.client.get('/account/register/', HTTP_ACCEPT_LANGUAGE='en')
self.assertRedirects(response, 'http://testserver/en/account/register/')
response = self.client.get(response['location'])
self.assertEqual(response.status_code, 200)
def test_en_redirect_wrong_url(self):
response = self.client.get('/profiel/registeren/', HTTP_ACCEPT_LANGUAGE='en')
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], 'http://testserver/en/profiel/registeren/')
response = self.client.get(response['location'])
self.assertEqual(response.status_code, 404)
def test_nl_redirect(self):
response = self.client.get('/profiel/registeren/', HTTP_ACCEPT_LANGUAGE='nl')
self.assertRedirects(response, 'http://testserver/nl/profiel/registeren/')
response = self.client.get(response['location'])
self.assertEqual(response.status_code, 200)
def test_nl_redirect_wrong_url(self):
response = self.client.get('/account/register/', HTTP_ACCEPT_LANGUAGE='nl')
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], 'http://testserver/nl/account/register/')
response = self.client.get(response['location'])
self.assertEqual(response.status_code, 404)
def test_pt_br_redirect(self):
response = self.client.get('/conta/registre-se/', HTTP_ACCEPT_LANGUAGE='pt-br')
self.assertRedirects(response, 'http://testserver/pt-br/conta/registre-se/')
response = self.client.get(response['location'])
self.assertEqual(response.status_code, 200)
class URLRedirectWithoutTrailingSlashTests(URLTestCaseBase):
"""
Tests the redirect when the requested URL doesn't end with a slash
(`settings.APPEND_SLASH=True`).
"""
def test_not_prefixed_redirect(self):
response = self.client.get('/not-prefixed', HTTP_ACCEPT_LANGUAGE='en')
self.assertEqual(response.status_code, 301)
self.assertEqual(response['location'], 'http://testserver/not-prefixed/')
def test_en_redirect(self):
response = self.client.get('/account/register', HTTP_ACCEPT_LANGUAGE='en')
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], 'http://testserver/en/account/register')
response = self.client.get(response['location'])
self.assertEqual(response.status_code, 301)
self.assertEqual(response['location'], 'http://testserver/en/account/register/')
class URLRedirectWithoutTrailingSlashSettingTests(URLTestCaseBase):
"""
Tests the redirect when the requested URL doesn't end with a slash
(`settings.APPEND_SLASH=False`).
"""
@override_settings(APPEND_SLASH=False)
def test_not_prefixed_redirect(self):
response = self.client.get('/not-prefixed', HTTP_ACCEPT_LANGUAGE='en')
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], 'http://testserver/en/not-prefixed')
response = self.client.get(response['location'])
self.assertEqual(response.status_code, 404)
@override_settings(APPEND_SLASH=False)
def test_en_redirect(self):
response = self.client.get('/account/register', HTTP_ACCEPT_LANGUAGE='en')
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], 'http://testserver/en/account/register')
response = self.client.get(response['location'])
self.assertEqual(response.status_code, 404)
class URLResponseTests(URLTestCaseBase):
"""
Tests if the response has the right language-code.
"""
def test_not_prefixed_with_prefix(self):
response = self.client.get('/en/not-prefixed/')
self.assertEqual(response.status_code, 404)
def test_en_url(self):
response = self.client.get('/en/account/register/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['content-language'], 'en')
self.assertEqual(response.context['LANGUAGE_CODE'], 'en')
def test_nl_url(self):
response = self.client.get('/nl/profiel/registeren/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['content-language'], 'nl')
self.assertEqual(response.context['LANGUAGE_CODE'], 'nl')
def test_wrong_en_prefix(self):
response = self.client.get('/en/profiel/registeren/')
self.assertEqual(response.status_code, 404)
def test_wrong_nl_prefix(self):
response = self.client.get('/nl/account/register/')
self.assertEqual(response.status_code, 404)
def test_pt_br_url(self):
response = self.client.get('/pt-br/conta/registre-se/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['content-language'], 'pt-br')
self.assertEqual(response.context['LANGUAGE_CODE'], 'pt-br')

View File

@ -0,0 +1,19 @@
from django.conf.urls.defaults import patterns, include, url
from django.conf.urls.i18n import i18n_patterns
from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView
view = TemplateView.as_view(template_name='dummy.html')
urlpatterns = patterns('',
url(r'^not-prefixed/$', view, name='not-prefixed'),
url(_(r'^translated/$'), view, name='no-prefix-translated'),
url(_(r'^translated/(?P<slug>[\w-]+)/$'), view, name='no-prefix-translated-slug'),
)
urlpatterns += i18n_patterns('',
url(r'^prefixed/$', view, name='prefixed'),
url(_(r'^users/$'), view, name='users'),
url(_(r'^account/'), include('regressiontests.i18n.patterns.urls.namespace', namespace='account')),
)

View File

@ -0,0 +1,9 @@
from django.conf.urls.defaults import url
from django.conf.urls.i18n import i18n_patterns
from django.views.generic import TemplateView
view = TemplateView.as_view(template_name='dummy.html')
urlpatterns = i18n_patterns('',
url(r'^prefixed/$', view, name='prefixed'),
)

View File

@ -0,0 +1,10 @@
from django.conf.urls.defaults import patterns, include, url
from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView
view = TemplateView.as_view(template_name='dummy.html')
urlpatterns = patterns('',
url(_(r'^register/$'), view, name='register'),
)

View File

@ -0,0 +1,8 @@
from django.conf.urls.defaults import patterns, include, url
from django.conf.urls.i18n import i18n_patterns
from django.utils.translation import ugettext_lazy as _
urlpatterns = i18n_patterns('',
url(_(r'^account/'), include('regressiontests.i18n.patterns.urls.wrong_namespace', namespace='account')),
)

View File

@ -0,0 +1,11 @@
from django.conf.urls.defaults import include, url
from django.conf.urls.i18n import i18n_patterns
from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView
view = TemplateView.as_view(template_name='dummy.html')
urlpatterns = i18n_patterns('',
url(_(r'^register/$'), view, name='register'),
)

View File

@ -3,13 +3,12 @@ from __future__ import with_statement
import datetime import datetime
import decimal import decimal
import os import os
import sys
import pickle import pickle
from threading import local from threading import local
from django.conf import settings from django.conf import settings
from django.template import Template, Context from django.template import Template, Context
from django.test import TestCase from django.test import TestCase, RequestFactory
from django.utils.formats import (get_format, date_format, time_format, from django.utils.formats import (get_format, date_format, time_format,
localize, localize_input, iter_format_modules, get_format_modules) localize, localize_input, iter_format_modules, get_format_modules)
from django.utils.importlib import import_module from django.utils.importlib import import_module
@ -18,14 +17,14 @@ from django.utils.safestring import mark_safe, SafeString, SafeUnicode
from django.utils import translation from django.utils import translation
from django.utils.translation import (ugettext, ugettext_lazy, activate, from django.utils.translation import (ugettext, ugettext_lazy, activate,
deactivate, gettext_lazy, pgettext, npgettext, to_locale, deactivate, gettext_lazy, pgettext, npgettext, to_locale,
get_language_info, get_language) get_language_info, get_language, get_language_from_request)
from forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm from forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm
from models import Company, TestModel from models import Company, TestModel
from commands.tests import * from commands.tests import *
from patterns.tests import *
from test_warnings import DeprecationWarningTests from test_warnings import DeprecationWarningTests
class TranslationTests(TestCase): class TranslationTests(TestCase):
@ -494,6 +493,9 @@ class FormattingTests(TestCase):
class MiscTests(TestCase): class MiscTests(TestCase):
def setUp(self):
self.rf = RequestFactory()
def test_parse_spec_http_header(self): def test_parse_spec_http_header(self):
""" """
Testing HTTP header parsing. First, we test that we can parse the Testing HTTP header parsing. First, we test that we can parse the
@ -534,10 +536,8 @@ class MiscTests(TestCase):
""" """
Now test that we parse a literal HTTP header correctly. Now test that we parse a literal HTTP header correctly.
""" """
from django.utils.translation.trans_real import get_language_from_request
g = get_language_from_request g = get_language_from_request
from django.http import HttpRequest r = self.rf.get('/')
r = HttpRequest
r.COOKIES = {} r.COOKIES = {}
r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt-br'} r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt-br'}
self.assertEqual('pt-br', g(r)) self.assertEqual('pt-br', g(r))
@ -569,10 +569,8 @@ class MiscTests(TestCase):
""" """
Now test that we parse language preferences stored in a cookie correctly. Now test that we parse language preferences stored in a cookie correctly.
""" """
from django.utils.translation.trans_real import get_language_from_request
g = get_language_from_request g = get_language_from_request
from django.http import HttpRequest r = self.rf.get('/')
r = HttpRequest
r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'pt-br'} r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'pt-br'}
r.META = {} r.META = {}
self.assertEqual('pt-br', g(r)) self.assertEqual('pt-br', g(r))
@ -827,4 +825,3 @@ class MultipleLocaleActivationTests(TestCase):
t = Template("{% load i18n %}{% blocktrans %}No{% endblocktrans %}") t = Template("{% load i18n %}{% blocktrans %}No{% endblocktrans %}")
with translation.override('nl'): with translation.override('nl'):
self.assertEqual(t.render(Context({})), 'Nee') self.assertEqual(t.render(Context({})), 'Nee')