Code

Ticket #11585: i18nurls.diff

File i18nurls.diff, 34.3 KB (added by brocaar, 3 years ago)

Implementation for language-prefix and internationalization of URL patterns

Line 
1Index: AUTHORS
2===================================================================
3--- AUTHORS     (revision 16384)
4+++ AUTHORS     (working copy)
5@@ -94,6 +94,7 @@
6     Sean Brant
7     Andrew Brehaut <http://brehaut.net/blog>
8     David Brenneman <http://davidbrenneman.com>
9+    Orne Brocaar <http://brocaar.com/>
10     brut.alll@gmail.com
11     bthomas
12     btoll@bestweb.net
13Index: docs/topics/i18n/internationalization.txt
14===================================================================
15--- docs/topics/i18n/internationalization.txt   (revision 16384)
16+++ docs/topics/i18n/internationalization.txt   (working copy)
17@@ -769,6 +769,120 @@
18 cases where you really need it (for example, in conjunction with ``ngettext``
19 to produce proper pluralizations).
20 
21+.. _url-internationalization:
22+
23+Specifying translation strings: In URL patterns
24+===============================================
25+
26+..  versionadded:: 1.4
27+
28+Django provides two meganism to internationalize URL patterns:
29+
30+* Adding the language-prefix to the root of the URL patterns to make it possible
31+  for the ``LocaleMiddleware`` to detect the language to activate from the requested
32+  URL.
33+
34+* Making URL patterns translatable by using the ``ugettext_lazy()`` function.
35+
36+
37+Language prefix in URL patterns
38+-------------------------------
39+
40+By using the ``i18n_patterns()`` function instead of the ``patterns()`` function,
41+Django will add the current active language automatically to all the URL patterns
42+defined within this function. Example URL patterns::
43+
44+    from django.conf.urls.defaults import patterns, i18n_patterns, include, url
45+   
46+    urlpatterns = patterns(''
47+        url(r'^sitemap\.xml$', 'sitemap.view', name='sitemap_xml'),
48+    )
49+   
50+    news_patterns = patterns(''
51+        url(r'^$', 'news.views.index', name='index'),
52+        url(r'^category/(?P<slug>[\w-]+)/$', 'news.views.category', name='category'),
53+        url(r'^(?P<slug>[\w-]+)/$', 'news.views.details', name='detail'),
54+    )
55+   
56+    urlpatterns += i18n_patterns('',
57+        url(r'^about/$', 'about.view', name='about'),
58+        url(r'^news/$', include(news_patterns, namespace='news')),
59+    )
60+
61+
62+After defining these URL patterns, Django will automatically add the language-prefix
63+to the URL patterns within the ``i18n_patterns`` function. Example::
64+
65+    from django.core.urlresolvers import reverse
66+    from django.utils.translation import activate
67+   
68+    >>> activate('en')
69+    >>> reverse('sitemap_xml')
70+    '/sitemap.xml'
71+    >>> reverse('news:index')
72+    '/en/news/'
73+   
74+    >>> activate('nl')
75+    >>> reverse('news:detail', kwargs={'slug': 'news-slug'})
76+    '/nl/news/news-slug/'
77+
78+.. warning::
79+
80+    * The ``i18n_patterns()`` function is only allowed at rootlevel of the URL patterns.
81+      When including a pattern which contains one or more ``i18n_patterns()`` patterns,
82+      an exception will be thrown.
83+   
84+    * Make sure the automatically added language-prefix does not cause collision
85+      with other non-prefixed URL patterns.
86+
87+
88+Translating URL patterns
89+------------------------
90+
91+URL patterns can be marked translatable by using the ``ugettext_lazy()`` function.
92+Example::
93+
94+    from django.conf.urls.defaults import patterns, i18n_patterns, include, url
95+    from django.utils.translation import ugettext_lazy
96+   
97+    urlpatterns = patterns(''
98+        url(r'^sitemap\.xml$', 'sitemap.view', name='sitemap_xml'),
99+    )
100+   
101+    news_patterns = patterns(''
102+        url(r'^$', 'news.views.index', name='index'),
103+        url(_(r'^category/(?P<slug>[\w-]+)/$'), 'news.views.category', name='category'),
104+        url(r'^(?P<slug>[\w-]+)/$', 'news.views.details', name='detail'),
105+    )
106+   
107+    urlpatterns += i18n_patterns('',
108+        url(_(r'^about/$'), 'about.view', name='about'),
109+        url(_(r'^news/$'), include(news_patterns, namespace='news')),
110+    )
111+
112+
113+After creating the translations (see :doc:`localization` for more information),
114+the ``reverse()`` function will return the URL in the active language. Example::
115+
116+    from django.core.urlresolvers import reverse
117+    from django.utils.translation import activate
118+   
119+    >>> activate('en')
120+    >>> reverse('news:category', kwargs={'slug': 'recent'})
121+    '/en/news/news-slug/'
122+   
123+    >>> activate('nl')
124+    >>> reverse('news:category', kwargs={'slug': 'recent'})
125+    '/nl/nieuws/categorie/recent/'
126+
127+.. warning::
128+
129+    Unless you are using your own middleware to detect the language to activate
130+    from the requested URL, always use translatable URL patterns together with
131+    the ``i18n_patterns()`` function. Django is not able to resolve a URL when
132+    the correct language is not activated.
133+
134+
135 .. _set_language-redirect-view:
136 
137 The ``set_language`` redirect view
138Index: docs/topics/i18n/deployment.txt
139===================================================================
140--- docs/topics/i18n/deployment.txt     (revision 16384)
141+++ docs/topics/i18n/deployment.txt     (working copy)
142@@ -59,7 +59,9 @@
143 
144     * Make sure it's one of the first middlewares installed.
145     * It should come after ``SessionMiddleware``, because ``LocaleMiddleware``
146-      makes use of session data.
147+      makes use of session data. As well it should come before ``CommonMiddleware``
148+      because ``CommonMiddleware`` depends on a activated language when it needs
149+      to resolve the requested URL.
150     * If you use ``CacheMiddleware``, put ``LocaleMiddleware`` after it.
151 
152 For example, your :setting:`MIDDLEWARE_CLASSES` might look like this::
153@@ -76,7 +78,12 @@
154 ``LocaleMiddleware`` tries to determine the user's language preference by
155 following this algorithm:
156 
157-    * First, it looks for a ``django_language`` key in the current user's
158+    * First, it looks for the language-prefix in the requested URL.
159+      This is only performed when you are using the ``i18n_patterns`` function.
160+      See :ref:`url-internationalization` for more information about the
161+      language-prefix and how to internationalize URL patterns.
162+
163+    * Failing that, it looks for a ``django_language`` key in the current user's
164       session.
165 
166     * Failing that, it looks for a cookie.
167Index: django/conf/urls/defaults.py
168===================================================================
169--- django/conf/urls/defaults.py        (revision 16384)
170+++ django/conf/urls/defaults.py        (working copy)
171@@ -1,6 +1,9 @@
172-from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
173+from django.core.urlresolvers import (RegexURLPattern, RegexURLResolver,
174+    LanguagePrefixedRegexURLResolver, get_resolver)
175 from django.core.exceptions import ImproperlyConfigured
176+from django.utils.importlib import import_module
177 
178+
179 __all__ = ['handler404', 'handler500', 'include', 'patterns', 'url']
180 
181 handler404 = 'django.views.defaults.page_not_found'
182@@ -15,6 +18,20 @@
183     else:
184         # No namespace hint - use manually provided namespace
185         urlconf_module = arg
186+   
187+    # Test if the LanguagePrefixedRegexURLResolver is used within the include,
188+    # this should throw an error since this is not allowed!
189+    if isinstance(urlconf_module, basestring):
190+        urlconf_module = import_module(urlconf_module)
191+    patterns = getattr(urlconf_module, 'urlpatterns', urlconf_module)
192+   
193+    # Make sure we can iterate through the patterns (without this, some testcases
194+    # will break).
195+    if isinstance(patterns, (list, tuple)):
196+        for url_pattern in patterns:
197+            if isinstance(url_pattern, LanguagePrefixedRegexURLResolver):
198+                raise ImproperlyConfigured('Using i18n_patterns inside an include is not allowed')
199+   
200     return (urlconf_module, app_name, namespace)
201 
202 def patterns(prefix, *args):
203@@ -27,6 +44,14 @@
204         pattern_list.append(t)
205     return pattern_list
206 
207+def i18n_patterns(prefix, *args):
208+    """
209+    This will add the language-code prefix to every URLPattern within this
210+    function. It is only allowed to use this at rootlevel of your URLConf.
211+    """
212+    pattern_list = patterns(prefix, *args)
213+    return [LanguagePrefixedRegexURLResolver(pattern_list)]
214+
215 def url(regex, view, kwargs=None, name=None, prefix=''):
216     if isinstance(view, (list,tuple)):
217         # For include(...) processing.
218@@ -39,4 +64,3 @@
219             if prefix:
220                 view = prefix + '.' + view
221         return RegexURLPattern(regex, view, kwargs, name)
222-
223Index: django/core/urlresolvers.py
224===================================================================
225--- django/core/urlresolvers.py (revision 16384)
226+++ django/core/urlresolvers.py (working copy)
227@@ -18,7 +18,9 @@
228 from django.utils.functional import memoize, lazy
229 from django.utils.importlib import import_module
230 from django.utils.regex_helper import normalize
231+from django.utils.translation import get_language
232 
233+
234 _resolver_cache = {} # Maps URLconf modules to RegexURLResolver instances.
235 _callable_cache = {} # Maps view and url pattern names to their view functions.
236 
237@@ -117,11 +119,14 @@
238 
239 class RegexURLPattern(object):
240     def __init__(self, regex, callback, default_args=None, name=None):
241-        # regex is a string representing a regular expression.
242+        # regex is eighter a string representing a regular expression, or a
243+        # translatable string (using ugettext_lazy) representing a regular
244+        # expression.
245         # callback is either a string like 'foo.views.news.stories.story_detail'
246         # which represents the path to a module and a view function name, or a
247         # callable object (view).
248-        self.regex = re.compile(regex, re.UNICODE)
249+        self._regex = regex
250+        self._i18n_regex_dict = {}
251         if callable(callback):
252             self._callback = callback
253         else:
254@@ -129,6 +134,24 @@
255             self._callback_str = callback
256         self.default_args = default_args or {}
257         self.name = name
258+   
259+    @property
260+    def regex(self):
261+        """
262+        Returns a compiled regular expression, depending upon the activated
263+        language-code.
264+        """
265+        language_code = get_language()
266+       
267+        if language_code not in self._i18n_regex_dict:
268+            if isinstance(self._regex, basestring):
269+                compiled_regex = re.compile(self._regex, re.UNICODE)
270+            else:
271+                regex = unicode(self._regex)
272+                compiled_regex = re.compile(regex, re.UNICODE)
273+            self._i18n_regex_dict[language_code] = compiled_regex
274+       
275+        return self._i18n_regex_dict[language_code]
276 
277     def __repr__(self):
278         return smart_str(u'<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern))
279@@ -175,7 +198,8 @@
280     def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
281         # regex is a string representing a regular expression.
282         # urlconf_name is a string representing the module containing URLconfs.
283-        self.regex = re.compile(regex, re.UNICODE)
284+        self._regex = regex
285+        self._i18n_regex_dict = {}
286         self.urlconf_name = urlconf_name
287         if not isinstance(urlconf_name, basestring):
288             self._urlconf_module = self.urlconf_name
289@@ -183,10 +207,28 @@
290         self.default_kwargs = default_kwargs or {}
291         self.namespace = namespace
292         self.app_name = app_name
293-        self._reverse_dict = None
294-        self._namespace_dict = None
295-        self._app_dict = None
296+        self._i18n_reverse_dict = {}
297+        self._i18n_namespace_dict = {}
298+        self._i18n_app_dict = {}
299 
300+    @property
301+    def regex(self):
302+        """
303+        Returns a compiled regular expression, depending upon the activated
304+        language-code.
305+        """
306+        language_code = get_language()
307+       
308+        if language_code not in self._i18n_regex_dict:
309+            if isinstance(self._regex, basestring):
310+                compiled_regex = re.compile(self._regex, re.UNICODE)
311+            else:
312+                regex = unicode(self._regex)
313+                compiled_regex = re.compile(regex, re.UNICODE)
314+            self._i18n_regex_dict[language_code] = compiled_regex
315+       
316+        return self._i18n_regex_dict[language_code]
317+
318     def __repr__(self):
319         return smart_str(u'<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern))
320 
321@@ -220,27 +262,32 @@
322                 lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args))
323                 if pattern.name is not None:
324                     lookups.appendlist(pattern.name, (bits, p_pattern, pattern.default_args))
325-        self._reverse_dict = lookups
326-        self._namespace_dict = namespaces
327-        self._app_dict = apps
328+       
329+        language_code = get_language()
330+        self._i18n_reverse_dict[language_code] = lookups
331+        self._i18n_namespace_dict[language_code] = namespaces
332+        self._i18n_app_dict[language_code] = apps
333 
334-    def _get_reverse_dict(self):
335-        if self._reverse_dict is None:
336+    @property
337+    def reverse_dict(self):
338+        language_code = get_language()
339+        if language_code not in self._i18n_reverse_dict:
340             self._populate()
341-        return self._reverse_dict
342-    reverse_dict = property(_get_reverse_dict)
343+        return self._i18n_reverse_dict[language_code]
344 
345-    def _get_namespace_dict(self):
346-        if self._namespace_dict is None:
347+    @property
348+    def namespace_dict(self):
349+        language_code = get_language()
350+        if language_code not in self._i18n_namespace_dict:
351             self._populate()
352-        return self._namespace_dict
353-    namespace_dict = property(_get_namespace_dict)
354+        return self._i18n_namespace_dict[language_code]
355 
356-    def _get_app_dict(self):
357-        if self._app_dict is None:
358+    @property
359+    def app_dict(self):
360+        language_code = get_language()
361+        if language_code not in self._i18n_app_dict:
362             self._populate()
363-        return self._app_dict
364-    app_dict = property(_get_app_dict)
365+        return self._i18n_app_dict[language_code]
366 
367     def resolve(self, path):
368         tried = []
369@@ -343,6 +390,25 @@
370         raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
371                 "arguments '%s' not found." % (lookup_view_s, args, kwargs))
372 
373+class LanguagePrefixedRegexURLResolver(RegexURLResolver):
374+    """
375+    The ``__init__`` function does not take an regex argument. Instead, we are
376+    overriding the ``regex`` function to always return the active language-code
377+    as regex.
378+    """
379+   
380+    def __init__(self, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
381+        super(LanguagePrefixedRegexURLResolver, self).__init__(None, urlconf_name,
382+            default_kwargs, app_name, namespace)
383+
384+    @property
385+    def regex(self):
386+        language_code = get_language()
387+        if language_code not in self._i18n_regex_dict:
388+            regex_compiled = re.compile('^%s/' % language_code, re.UNICODE)
389+            self._i18n_regex_dict[language_code] = regex_compiled
390+        return self._i18n_regex_dict[language_code]
391+
392 def resolve(path, urlconf=None):
393     if urlconf is None:
394         urlconf = get_urlconf()
395Index: django/middleware/locale.py
396===================================================================
397--- django/middleware/locale.py (revision 16384)
398+++ django/middleware/locale.py (working copy)
399@@ -1,8 +1,16 @@
400 "this is the locale selecting middleware that will look at accept headers"
401 
402+import re
403+
404+from django.conf import settings
405+from django.core import urlresolvers
406+from django.http import HttpResponseRedirect
407 from django.utils.cache import patch_vary_headers
408 from django.utils import translation
409 
410+
411+language_code_prefix_regex = re.compile(r'^/([\w-]+)/')
412+
413 class LocaleMiddleware(object):
414     """
415     This is a very simple middleware that parses a request
416@@ -13,13 +21,46 @@
417     """
418 
419     def process_request(self, request):
420-        language = translation.get_language_from_request(request)
421+        language = _language_code_from_path(request.path_info)
422+       
423+        if not language:
424+            language = translation.get_language_from_request(request)
425+       
426         translation.activate(language)
427         request.LANGUAGE_CODE = translation.get_language()
428 
429     def process_response(self, request, response):
430+        language = translation.get_language()
431+        translation.deactivate()
432+       
433+        if (response.status_code == 404 and not _language_code_from_path(request.path_info)
434+            and _is_language_prefix_patterns_used()):
435+            prefixed_request_path = '/%s%s' % (language, request.get_full_path())
436+            return HttpResponseRedirect(prefixed_request_path)
437+       
438         patch_vary_headers(response, ('Accept-Language',))
439         if 'Content-Language' not in response:
440-            response['Content-Language'] = translation.get_language()
441-        translation.deactivate()
442+            response['Content-Language'] = language
443         return response
444+
445+def _language_code_from_path(path):
446+    """
447+    Returns the language-code if there is a valid language-code found in the
448+    `path`.
449+    """
450+    regex_match = language_code_prefix_regex.match(path)
451+    if regex_match:
452+        if regex_match.group(1) in dict(settings.LANGUAGES):
453+            return regex_match.group(1)
454+    return None
455+
456+def _is_language_prefix_patterns_used():
457+    """
458+    Returns `True` if the `LanguagePrefixedRegexURLResolver` is used at rootlevel
459+    of the urlpatterns, else it returns `False`.
460+    """
461+    resolver = urlresolvers.get_resolver(None)
462+    for url_pattern in resolver.url_patterns:
463+        if isinstance(url_pattern, urlresolvers.LanguagePrefixedRegexURLResolver):
464+            return True
465+    return False
466Index: tests/regressiontests/i18n/wrong_urls_namespace.py
467===================================================================
468--- tests/regressiontests/i18n/wrong_urls_namespace.py  (revision 0)
469+++ tests/regressiontests/i18n/wrong_urls_namespace.py  (revision 0)
470@@ -0,0 +1,10 @@
471+from django.conf.urls.defaults import i18n_patterns, include, url
472+from django.utils.translation import ugettext_lazy as _
473+from django.views.generic import TemplateView
474+
475+
476+view = TemplateView.as_view(template_name='dummy.html')
477+
478+urlpatterns = i18n_patterns('',
479+    url(_(r'^register/$'), view, name='register'),
480+)
481Index: tests/regressiontests/i18n/urls_namespace.py
482===================================================================
483--- tests/regressiontests/i18n/urls_namespace.py        (revision 0)
484+++ tests/regressiontests/i18n/urls_namespace.py        (revision 0)
485@@ -0,0 +1,10 @@
486+from django.conf.urls.defaults import patterns, include, url
487+from django.utils.translation import ugettext_lazy as _
488+from django.views.generic import TemplateView
489+
490+
491+view = TemplateView.as_view(template_name='dummy.html')
492+
493+urlpatterns = patterns('',
494+    url(_(r'^register/$'), view, name='register'),
495+)
496Index: tests/regressiontests/i18n/wrong_urls.py
497===================================================================
498--- tests/regressiontests/i18n/wrong_urls.py    (revision 0)
499+++ tests/regressiontests/i18n/wrong_urls.py    (revision 0)
500@@ -0,0 +1,7 @@
501+from django.conf.urls.defaults import patterns, i18n_patterns, include, url
502+from django.utils.translation import ugettext_lazy as _
503+
504+
505+urlpatterns = i18n_patterns('',
506+    url(_(r'^account/'), include('regressiontests.i18n.wrong_urls_namespace', namespace='account')),
507+)
508Index: tests/regressiontests/i18n/tests.py
509===================================================================
510--- tests/regressiontests/i18n/tests.py (revision 16384)
511+++ tests/regressiontests/i18n/tests.py (working copy)
512@@ -8,8 +8,11 @@
513 from threading import local
514 
515 from django.conf import settings
516+from django.core.exceptions import ImproperlyConfigured
517+from django.core.urlresolvers import reverse, get_resolver, clear_url_caches
518 from django.template import Template, Context
519 from django.test import TestCase
520+from django.test.utils import override_settings
521 from django.utils.formats import (get_format, date_format, time_format,
522     localize, localize_input, iter_format_modules, get_format_modules)
523 from django.utils.importlib import import_module
524@@ -828,3 +831,217 @@
525         with translation.override('nl'):
526             self.assertEqual(t.render(Context({})), 'Nee')
527 
528+
529+class URLTestCaseBase(TestCase):
530+    """
531+    TestCase base-class for the URL tests.
532+    """
533+    def setUp(self):
534+        # Make sure the cache is empty before we are doing our tests.
535+        clear_url_caches()
536+   
537+    def tearDown(self):
538+        # Make sure we will leave an empty cache for other testcases.
539+        clear_url_caches()
540+
541+
542+URLTestCaseBase = override_settings(
543+    ROOT_URLCONF='regressiontests.i18n.urls',
544+    LOCALE_PATHS=(os.path.join(os.path.dirname(__file__), 'test_locale'), ),
545+    TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'test_templates'), ),
546+    LANGUAGE_CODE='en',
547+    LANGUAGES=(
548+        ('nl', 'Dutch'),
549+        ('en', 'English'),
550+        ('pt-br', 'Brazilian Portuguese'),
551+    ),
552+    MIDDLEWARE_CLASSES=(
553+        'django.middleware.locale.LocaleMiddleware',
554+        'django.middleware.common.CommonMiddleware',
555+    ),
556+)(URLTestCaseBase)
557+
558+
559+class URLPrefixTests(URLTestCaseBase):
560+    """
561+    Tests if the `i18n_patterns` is adding the prefix correctly.
562+    """
563+    def test_not_prefixed(self):
564+        with translation.override('en'):
565+            self.assertEqual(reverse('not-prefixed'), '/not-prefixed/')
566+        with translation.override('nl'):
567+            self.assertEqual(reverse('not-prefixed'), '/not-prefixed/')
568+
569+    def test_prefixed(self):
570+        with translation.override('en'):
571+            self.assertEqual(reverse('prefixed'), '/en/prefixed/')
572+        with translation.override('nl'):
573+            self.assertEqual(reverse('prefixed'), '/nl/prefixed/')
574+   
575+    @override_settings(ROOT_URLCONF='regressiontests.i18n.wrong_urls')
576+    def test_invalid_prefix_use(self):
577+        self.assertRaises(ImproperlyConfigured, lambda: reverse('account:register'))
578+
579+
580+class URLTranslationTests(URLTestCaseBase):
581+    """
582+    Tests if the pattern-strings are translated correctly (within the
583+    `i18n_patterns` and the normal `patterns` function).
584+    """
585+    def test_no_prefix_translated(self):
586+        with translation.override('en'):
587+            self.assertEqual(reverse('no-prefix-translated'), '/translated/')
588+
589+        with translation.override('nl'):
590+            self.assertEqual(reverse('no-prefix-translated'), '/vertaald/')
591+
592+        with translation.override('pt-br'):
593+            self.assertEqual(reverse('no-prefix-translated'), '/traduzidos/')
594+
595+    def test_users_url(self):
596+        with translation.override('en'):
597+            self.assertEqual(reverse('users'), '/en/users/')
598+
599+        with translation.override('nl'):
600+            self.assertEqual(reverse('users'), '/nl/gebruikers/')
601+
602+        with translation.override('pt-br'):
603+            self.assertEqual(reverse('users'), '/pt-br/usuarios/')
604+
605+
606+class URLNamespaceTests(URLTestCaseBase):
607+    """
608+    Tests if the translations are still working within namespaces.
609+    """
610+    def test_account_register(self):
611+        with translation.override('en'):
612+            self.assertEqual(reverse('account:register'), '/en/account/register/')
613+
614+        with translation.override('nl'):
615+            self.assertEqual(reverse('account:register'), '/nl/profiel/registeren/')
616+
617+
618+class URLRedirectTests(URLTestCaseBase):
619+    """
620+    Tests if the user gets redirected to the right URL when there is no
621+    language-prefix in the request URL.
622+    """
623+    def test_no_prefix_response(self):
624+        response = self.client.get('/not-prefixed/')
625+        self.assertEqual(response.status_code, 200)
626+
627+    def test_en_redirect(self):
628+        response = self.client.get('/account/register/', HTTP_ACCEPT_LANGUAGE='en')
629+        self.assertRedirects(response, 'http://testserver/en/account/register/')
630+
631+        response = self.client.get(response['location'])
632+        self.assertEqual(response.status_code, 200)
633+
634+    def test_en_redirect_wrong_url(self):
635+        response = self.client.get('/profiel/registeren/', HTTP_ACCEPT_LANGUAGE='en')
636+        self.assertEqual(response.status_code, 302)
637+        self.assertEqual(response['location'], 'http://testserver/en/profiel/registeren/')
638+
639+        response = self.client.get(response['location'])
640+        self.assertEqual(response.status_code, 404)
641+
642+    def test_nl_redirect(self):
643+        response = self.client.get('/profiel/registeren/', HTTP_ACCEPT_LANGUAGE='nl')
644+        self.assertRedirects(response, 'http://testserver/nl/profiel/registeren/')
645+
646+        response = self.client.get(response['location'])
647+        self.assertEqual(response.status_code, 200)
648+
649+    def test_nl_redirect_wrong_url(self):
650+        response = self.client.get('/account/register/', HTTP_ACCEPT_LANGUAGE='nl')
651+        self.assertEqual(response.status_code, 302)
652+        self.assertEqual(response['location'], 'http://testserver/nl/account/register/')
653+
654+        response = self.client.get(response['location'])
655+        self.assertEqual(response.status_code, 404)
656+
657+    def test_pt_br_redirect(self):
658+        response = self.client.get('/conta/registre-se/', HTTP_ACCEPT_LANGUAGE='pt-br')
659+        self.assertRedirects(response, 'http://testserver/pt-br/conta/registre-se/')
660+
661+        response = self.client.get(response['location'])
662+        self.assertEqual(response.status_code, 200)
663+
664+
665+class URLRedirectWithoutTrailingSlashTests(URLTestCaseBase):
666+    """
667+    Tests the redirect when the requested URL doesn't end with a slash
668+    (`settings.APPEND_SLASH=True`).
669+    """
670+    def test_not_prefixed_redirect(self):
671+        response = self.client.get('/not-prefixed', HTTP_ACCEPT_LANGUAGE='en')
672+        self.assertEqual(response.status_code, 301)
673+        self.assertEqual(response['location'], 'http://testserver/not-prefixed/')
674+
675+    def test_en_redirect(self):
676+        response = self.client.get('/account/register', HTTP_ACCEPT_LANGUAGE='en')
677+        self.assertEqual(response.status_code, 302)
678+        self.assertEqual(response['location'], 'http://testserver/en/account/register')
679+
680+        response = self.client.get(response['location'])
681+        self.assertEqual(response.status_code, 301)
682+        self.assertEqual(response['location'], 'http://testserver/en/account/register/')
683+
684+
685+class URLRedirectWithoutTrailingSlashSettingTests(URLTestCaseBase):
686+    """
687+    Tests the redirect when the requested URL doesn't end with a slash
688+    (`settings.APPEND_SLASH=False`).
689+    """
690+    @override_settings(APPEND_SLASH=False)
691+    def test_not_prefixed_redirect(self):
692+        response = self.client.get('/not-prefixed', HTTP_ACCEPT_LANGUAGE='en')
693+        self.assertEqual(response.status_code, 302)
694+        self.assertEqual(response['location'], 'http://testserver/en/not-prefixed')
695+
696+        response = self.client.get(response['location'])
697+        self.assertEqual(response.status_code, 404)
698+
699+    @override_settings(APPEND_SLASH=False)
700+    def test_en_redirect(self):
701+        response = self.client.get('/account/register', HTTP_ACCEPT_LANGUAGE='en')
702+        self.assertEqual(response.status_code, 302)
703+        self.assertEqual(response['location'], 'http://testserver/en/account/register')
704+
705+        response = self.client.get(response['location'])
706+        self.assertEqual(response.status_code, 404)
707+
708+
709+class URLResponseTests(URLTestCaseBase):
710+    """
711+    Tests if the response has the right language-code.
712+    """
713+    def test_not_prefixed_with_prefix(self):
714+        response = self.client.get('/en/not-prefixed/')
715+        self.assertEqual(response.status_code, 404)
716+
717+    def test_en_url(self):
718+        response = self.client.get('/en/account/register/')
719+        self.assertEqual(response.status_code, 200)
720+        self.assertEqual(response['content-language'], 'en')
721+        self.assertEqual(response.context['LANGUAGE_CODE'], 'en')
722+
723+    def test_nl_url(self):
724+        response = self.client.get('/nl/profiel/registeren/')
725+        self.assertEqual(response.status_code, 200)
726+        self.assertEqual(response['content-language'], 'nl')
727+        self.assertEqual(response.context['LANGUAGE_CODE'], 'nl')
728+
729+    def test_wrong_en_prefix(self):
730+        response = self.client.get('/en/profiel/registeren/')
731+        self.assertEqual(response.status_code, 404)
732+
733+    def test_wrong_nl_prefix(self):
734+        response = self.client.get('/nl/account/register/')
735+        self.assertEqual(response.status_code, 404)
736+
737+    def test_pt_br_url(self):
738+        response = self.client.get('/pt-br/conta/registre-se/')
739+        self.assertEqual(response.status_code, 200)
740+        self.assertEqual(response['content-language'], 'pt-br')
741+        self.assertEqual(response.context['LANGUAGE_CODE'], 'pt-br')
742Index: tests/regressiontests/i18n/urls.py
743===================================================================
744--- tests/regressiontests/i18n/urls.py  (revision 0)
745+++ tests/regressiontests/i18n/urls.py  (revision 0)
746@@ -0,0 +1,17 @@
747+from django.conf.urls.defaults import patterns, i18n_patterns, include, url
748+from django.utils.translation import ugettext_lazy as _
749+from django.views.generic import TemplateView
750+
751+
752+view = TemplateView.as_view(template_name='dummy.html')
753+
754+urlpatterns = patterns('',
755+    url(r'^not-prefixed/$', view, name='not-prefixed'),
756+    url(_(r'^translated/$'), view, name='no-prefix-translated'),
757+)
758+
759+urlpatterns += i18n_patterns('',
760+    url(r'^prefixed/$', view, name='prefixed'),
761+    url(_(r'^users/$'), view, name='users'),
762+    url(_(r'^account/'), include('regressiontests.i18n.urls_namespace', namespace='account')),
763+)
764Index: tests/regressiontests/i18n/test_templates/404.html
765===================================================================
766Index: tests/regressiontests/i18n/test_templates/dummy.html
767===================================================================
768Index: tests/regressiontests/i18n/test_locale/en/LC_MESSAGES/django.po
769===================================================================
770--- tests/regressiontests/i18n/test_locale/en/LC_MESSAGES/django.po     (revision 0)
771+++ tests/regressiontests/i18n/test_locale/en/LC_MESSAGES/django.po     (revision 0)
772@@ -0,0 +1,34 @@
773+# SOME DESCRIPTIVE TITLE.
774+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
775+# This file is distributed under the same license as the PACKAGE package.
776+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
777+#
778+#, fuzzy
779+msgid ""
780+msgstr ""
781+"Project-Id-Version: PACKAGE VERSION\n"
782+"Report-Msgid-Bugs-To: \n"
783+"POT-Creation-Date: 2011-06-10 11:53+0200\n"
784+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
785+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
786+"Language-Team: LANGUAGE <LL@li.org>\n"
787+"Language: \n"
788+"MIME-Version: 1.0\n"
789+"Content-Type: text/plain; charset=UTF-8\n"
790+"Content-Transfer-Encoding: 8bit\n"
791+
792+#: urls.py:10
793+msgid "^translated/$"
794+msgstr "^translated/$"
795+
796+#: urls.py:15
797+msgid "^users/$"
798+msgstr "^users/$"
799+
800+#: urls.py:16
801+msgid "^account/"
802+msgstr "^account/"
803+
804+#: urls_namespace.py:9
805+msgid "^register/$"
806+msgstr "^register/$"
807Index: tests/regressiontests/i18n/test_locale/en/LC_MESSAGES/django.mo
808===================================================================
809Cannot display: file marked as a binary type.
810svn:mime-type = application/octet-stream
811
812Property changes on: tests/regressiontests/i18n/test_locale/en/LC_MESSAGES/django.mo
813___________________________________________________________________
814Added: svn:mime-type
815   + application/octet-stream
816
817Index: tests/regressiontests/i18n/test_locale/pt_BR/LC_MESSAGES/django.po
818===================================================================
819--- tests/regressiontests/i18n/test_locale/pt_BR/LC_MESSAGES/django.po  (revision 0)
820+++ tests/regressiontests/i18n/test_locale/pt_BR/LC_MESSAGES/django.po  (revision 0)
821@@ -0,0 +1,35 @@
822+# SOME DESCRIPTIVE TITLE.
823+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
824+# This file is distributed under the same license as the PACKAGE package.
825+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
826+#
827+#, fuzzy
828+msgid ""
829+msgstr ""
830+"Project-Id-Version: PACKAGE VERSION\n"
831+"Report-Msgid-Bugs-To: \n"
832+"POT-Creation-Date: 2011-06-10 13:56+0200\n"
833+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
834+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
835+"Language-Team: LANGUAGE <LL@li.org>\n"
836+"Language: \n"
837+"MIME-Version: 1.0\n"
838+"Content-Type: text/plain; charset=UTF-8\n"
839+"Content-Transfer-Encoding: 8bit\n"
840+"Plural-Forms: nplurals=2; plural=(n > 1)\n"
841+
842+#: urls.py:10
843+msgid "^translated/$"
844+msgstr "^traduzidos/$"
845+
846+#: urls.py:15
847+msgid "^users/$"
848+msgstr "^usuarios/$"
849+
850+#: urls.py:16 wrong_urls.py:6
851+msgid "^account/"
852+msgstr "^conta/"
853+
854+#: urls_namespace.py:9 wrong_urls_namespace.py:9
855+msgid "^register/$"
856+msgstr "^registre-se/$"
857Index: tests/regressiontests/i18n/test_locale/pt_BR/LC_MESSAGES/django.mo
858===================================================================
859Cannot display: file marked as a binary type.
860svn:mime-type = application/octet-stream
861
862Property changes on: tests/regressiontests/i18n/test_locale/pt_BR/LC_MESSAGES/django.mo
863___________________________________________________________________
864Added: svn:mime-type
865   + application/octet-stream
866
867Index: tests/regressiontests/i18n/test_locale/nl/LC_MESSAGES/django.po
868===================================================================
869--- tests/regressiontests/i18n/test_locale/nl/LC_MESSAGES/django.po     (revision 0)
870+++ tests/regressiontests/i18n/test_locale/nl/LC_MESSAGES/django.po     (revision 0)
871@@ -0,0 +1,35 @@
872+# SOME DESCRIPTIVE TITLE.
873+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
874+# This file is distributed under the same license as the PACKAGE package.
875+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
876+#
877+#, fuzzy
878+msgid ""
879+msgstr ""
880+"Project-Id-Version: PACKAGE VERSION\n"
881+"Report-Msgid-Bugs-To: \n"
882+"POT-Creation-Date: 2011-06-10 11:53+0200\n"
883+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
884+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
885+"Language-Team: LANGUAGE <LL@li.org>\n"
886+"Language: \n"
887+"MIME-Version: 1.0\n"
888+"Content-Type: text/plain; charset=UTF-8\n"
889+"Content-Transfer-Encoding: 8bit\n"
890+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
891+
892+#: urls.py:10
893+msgid "^translated/$"
894+msgstr "^vertaald/$"
895+
896+#: urls.py:15
897+msgid "^users/$"
898+msgstr "^gebruikers/$"
899+
900+#: urls.py:16
901+msgid "^account/"
902+msgstr "^profiel/"
903+
904+#: urls_namespace.py:9
905+msgid "^register/$"
906+msgstr "^registeren/$"
907Index: tests/regressiontests/i18n/test_locale/nl/LC_MESSAGES/django.mo
908===================================================================
909Cannot display: file marked as a binary type.
910svn:mime-type = application/octet-stream
911
912Property changes on: tests/regressiontests/i18n/test_locale/nl/LC_MESSAGES/django.mo
913___________________________________________________________________
914Added: svn:mime-type
915   + application/octet-stream
916