Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #11585 -- Added ability to translate and prefix URL patterns wi…

…th 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
  • Loading branch information...
commit 896e3c69c7eec311085da349a329ee80c8fca132 1 parent 62bb4b8
Jannis Leidel authored June 15, 2011

Showing 29 changed files with 751 additions and 58 deletions. Show diff stats Hide diff stats

  1. 1  AUTHORS
  2. 20  django/conf/urls/defaults.py
  3. 17  django/conf/urls/i18n.py
  4. 6  django/contrib/admindocs/views.py
  5. 121  django/core/urlresolvers.py
  6. 26  django/middleware/locale.py
  7. 3  django/utils/translation/__init__.py
  8. 4  django/utils/translation/trans_null.py
  9. 24  django/utils/translation/trans_real.py
  10. 10  docs/releases/1.4.txt
  11. 15  docs/topics/i18n/deployment.txt
  12. 132  docs/topics/i18n/internationalization.txt
  13. 0  tests/regressiontests/i18n/patterns/__init__.py
  14. BIN  tests/regressiontests/i18n/patterns/locale/en/LC_MESSAGES/django.mo
  15. 37  tests/regressiontests/i18n/patterns/locale/en/LC_MESSAGES/django.po
  16. BIN  tests/regressiontests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo
  17. 38  tests/regressiontests/i18n/patterns/locale/nl/LC_MESSAGES/django.po
  18. BIN  tests/regressiontests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.mo
  19. 38  tests/regressiontests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.po
  20. 0  tests/regressiontests/i18n/patterns/templates/404.html
  21. 0  tests/regressiontests/i18n/patterns/templates/dummy.html
  22. 241  tests/regressiontests/i18n/patterns/tests.py
  23. 0  tests/regressiontests/i18n/patterns/urls/__init__.py
  24. 19  tests/regressiontests/i18n/patterns/urls/default.py
  25. 9  tests/regressiontests/i18n/patterns/urls/disabled.py
  26. 10  tests/regressiontests/i18n/patterns/urls/namespace.py
  27. 8  tests/regressiontests/i18n/patterns/urls/wrong.py
  28. 11  tests/regressiontests/i18n/patterns/urls/wrong_namespace.py
  29. 19  tests/regressiontests/i18n/tests.py
1  AUTHORS
@@ -94,6 +94,7 @@ answer newbie questions, and generally made Django that much better:
94 94
     Sean Brant
95 95
     Andrew Brehaut <http://brehaut.net/blog>
96 96
     David Brenneman <http://davidbrenneman.com>
  97
+    Orne Brocaar <http://brocaar.com/>
97 98
     brut.alll@gmail.com
98 99
     bthomas
99 100
     btoll@bestweb.net
20  django/conf/urls/defaults.py
... ...
@@ -1,5 +1,8 @@
1  
-from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
  1
+from django.core.urlresolvers import (RegexURLPattern,
  2
+    RegexURLResolver, LocaleRegexURLResolver)
2 3
 from django.core.exceptions import ImproperlyConfigured
  4
+from django.utils.importlib import import_module
  5
+
3 6
 
4 7
 __all__ = ['handler404', 'handler500', 'include', 'patterns', 'url']
5 8
 
@@ -15,6 +18,21 @@ def include(arg, namespace=None, app_name=None):
15 18
     else:
16 19
         # No namespace hint - use manually provided namespace
17 20
         urlconf_module = arg
  21
+
  22
+    if isinstance(urlconf_module, basestring):
  23
+        urlconf_module = import_module(urlconf_module)
  24
+    patterns = getattr(urlconf_module, 'urlpatterns', urlconf_module)
  25
+
  26
+    # Make sure we can iterate through the patterns (without this, some
  27
+    # testcases will break).
  28
+    if isinstance(patterns, (list, tuple)):
  29
+        for url_pattern in patterns:
  30
+            # Test if the LocaleRegexURLResolver is used within the include;
  31
+            # this should throw an error since this is not allowed!
  32
+            if isinstance(url_pattern, LocaleRegexURLResolver):
  33
+                raise ImproperlyConfigured(
  34
+                    'Using i18n_patterns in an included URLconf is not allowed.')
  35
+
18 36
     return (urlconf_module, app_name, namespace)
19 37
 
20 38
 def patterns(prefix, *args):
17  django/conf/urls/i18n.py
... ...
@@ -1,4 +1,19 @@
1  
-from django.conf.urls.defaults import *
  1
+from django.conf import settings
  2
+from django.conf.urls.defaults import patterns
  3
+from django.core.urlresolvers import LocaleRegexURLResolver
  4
+
  5
+def i18n_patterns(prefix, *args):
  6
+    """
  7
+    Adds the language code prefix to every URL pattern within this
  8
+    function. This may only be used in the root URLconf, not in an included
  9
+    URLconf.
  10
+
  11
+    """
  12
+    pattern_list = patterns(prefix, *args)
  13
+    if not settings.USE_I18N:
  14
+        return pattern_list
  15
+    return [LocaleRegexURLResolver(pattern_list)]
  16
+
2 17
 
3 18
 urlpatterns = patterns('',
4 19
     (r'^setlang/$', 'django.views.i18n.set_language'),
6  django/contrib/admindocs/views.py
@@ -346,12 +346,12 @@ def extract_views_from_urlpatterns(urlpatterns, base=''):
346 346
     """
347 347
     views = []
348 348
     for p in urlpatterns:
349  
-        if hasattr(p, '_get_callback'):
  349
+        if hasattr(p, 'callback'):
350 350
             try:
351  
-                views.append((p._get_callback(), base + p.regex.pattern))
  351
+                views.append((p.callback, base + p.regex.pattern))
352 352
             except ViewDoesNotExist:
353 353
                 continue
354  
-        elif hasattr(p, '_get_url_patterns'):
  354
+        elif hasattr(p, 'url_patterns'):
355 355
             try:
356 356
                 patterns = p.url_patterns
357 357
             except ImportError:
121  django/core/urlresolvers.py
@@ -11,13 +11,14 @@
11 11
 from threading import local
12 12
 
13 13
 from django.http import Http404
14  
-from django.conf import settings
15 14
 from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
16 15
 from django.utils.datastructures import MultiValueDict
17 16
 from django.utils.encoding import iri_to_uri, force_unicode, smart_str
18 17
 from django.utils.functional import memoize, lazy
19 18
 from django.utils.importlib import import_module
20 19
 from django.utils.regex_helper import normalize
  20
+from django.utils.translation import get_language
  21
+
21 22
 
22 23
 _resolver_cache = {} # Maps URLconf modules to RegexURLResolver instances.
23 24
 _callable_cache = {} # Maps view and url pattern names to their view functions.
@@ -50,13 +51,13 @@ def __init__(self, func, args, kwargs, url_name=None, app_name=None, namespaces=
50 51
                 url_name = '.'.join([func.__module__, func.__name__])
51 52
         self.url_name = url_name
52 53
 
  54
+    @property
53 55
     def namespace(self):
54 56
         return ':'.join(self.namespaces)
55  
-    namespace = property(namespace)
56 57
 
  58
+    @property
57 59
     def view_name(self):
58 60
         return ':'.join([ x for x in [ self.namespace, self.url_name ]  if x ])
59  
-    view_name = property(view_name)
60 61
 
61 62
     def __getitem__(self, index):
62 63
         return (self.func, self.args, self.kwargs)[index]
@@ -115,13 +116,43 @@ def get_mod_func(callback):
115 116
         return callback, ''
116 117
     return callback[:dot], callback[dot+1:]
117 118
 
118  
-class RegexURLPattern(object):
  119
+class LocaleRegexProvider(object):
  120
+    """
  121
+    A mixin to provide a default regex property which can vary by active
  122
+    language.
  123
+
  124
+    """
  125
+    def __init__(self, regex):
  126
+        # regex is either a string representing a regular expression, or a
  127
+        # translatable string (using ugettext_lazy) representing a regular
  128
+        # expression.
  129
+        self._regex = regex
  130
+        self._regex_dict = {}
  131
+
  132
+
  133
+    @property
  134
+    def regex(self):
  135
+        """
  136
+        Returns a compiled regular expression, depending upon the activated
  137
+        language-code.
  138
+        """
  139
+        language_code = get_language()
  140
+        if language_code not in self._regex_dict:
  141
+            if isinstance(self._regex, basestring):
  142
+                compiled_regex = re.compile(self._regex, re.UNICODE)
  143
+            else:
  144
+                regex = force_unicode(self._regex)
  145
+                compiled_regex = re.compile(regex, re.UNICODE)
  146
+            self._regex_dict[language_code] = compiled_regex
  147
+        return self._regex_dict[language_code]
  148
+
  149
+
  150
+class RegexURLPattern(LocaleRegexProvider):
119 151
     def __init__(self, regex, callback, default_args=None, name=None):
120  
-        # regex is a string representing a regular expression.
  152
+        LocaleRegexProvider.__init__(self, regex)
121 153
         # callback is either a string like 'foo.views.news.stories.story_detail'
122 154
         # which represents the path to a module and a view function name, or a
123 155
         # callable object (view).
124  
-        self.regex = re.compile(regex, re.UNICODE)
125 156
         if callable(callback):
126 157
             self._callback = callback
127 158
         else:
@@ -157,7 +188,8 @@ def resolve(self, path):
157 188
 
158 189
             return ResolverMatch(self.callback, args, kwargs, self.name)
159 190
 
160  
-    def _get_callback(self):
  191
+    @property
  192
+    def callback(self):
161 193
         if self._callback is not None:
162 194
             return self._callback
163 195
         try:
@@ -169,13 +201,11 @@ def _get_callback(self):
169 201
             mod_name, func_name = get_mod_func(self._callback_str)
170 202
             raise ViewDoesNotExist("Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e)))
171 203
         return self._callback
172  
-    callback = property(_get_callback)
173 204
 
174  
-class RegexURLResolver(object):
  205
+class RegexURLResolver(LocaleRegexProvider):
175 206
     def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
176  
-        # regex is a string representing a regular expression.
  207
+        LocaleRegexProvider.__init__(self, regex)
177 208
         # urlconf_name is a string representing the module containing URLconfs.
178  
-        self.regex = re.compile(regex, re.UNICODE)
179 209
         self.urlconf_name = urlconf_name
180 210
         if not isinstance(urlconf_name, basestring):
181 211
             self._urlconf_module = self.urlconf_name
@@ -183,9 +213,9 @@ def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, name
183 213
         self.default_kwargs = default_kwargs or {}
184 214
         self.namespace = namespace
185 215
         self.app_name = app_name
186  
-        self._reverse_dict = None
187  
-        self._namespace_dict = None
188  
-        self._app_dict = None
  216
+        self._reverse_dict = {}
  217
+        self._namespace_dict = {}
  218
+        self._app_dict = {}
189 219
 
190 220
     def __repr__(self):
191 221
         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 @@ def _populate(self):
194 224
         lookups = MultiValueDict()
195 225
         namespaces = {}
196 226
         apps = {}
  227
+        language_code = get_language()
197 228
         for pattern in reversed(self.url_patterns):
198 229
             p_pattern = pattern.regex.pattern
199 230
             if p_pattern.startswith('^'):
@@ -220,27 +251,30 @@ def _populate(self):
220 251
                 lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args))
221 252
                 if pattern.name is not None:
222 253
                     lookups.appendlist(pattern.name, (bits, p_pattern, pattern.default_args))
223  
-        self._reverse_dict = lookups
224  
-        self._namespace_dict = namespaces
225  
-        self._app_dict = apps
226  
-
227  
-    def _get_reverse_dict(self):
228  
-        if self._reverse_dict is None:
  254
+        self._reverse_dict[language_code] = lookups
  255
+        self._namespace_dict[language_code] = namespaces
  256
+        self._app_dict[language_code] = apps
  257
+
  258
+    @property
  259
+    def reverse_dict(self):
  260
+        language_code = get_language()
  261
+        if language_code not in self._reverse_dict:
229 262
             self._populate()
230  
-        return self._reverse_dict
231  
-    reverse_dict = property(_get_reverse_dict)
  263
+        return self._reverse_dict[language_code]
232 264
 
233  
-    def _get_namespace_dict(self):
234  
-        if self._namespace_dict is None:
  265
+    @property
  266
+    def namespace_dict(self):
  267
+        language_code = get_language()
  268
+        if language_code not in self._namespace_dict:
235 269
             self._populate()
236  
-        return self._namespace_dict
237  
-    namespace_dict = property(_get_namespace_dict)
  270
+        return self._namespace_dict[language_code]
238 271
 
239  
-    def _get_app_dict(self):
240  
-        if self._app_dict is None:
  272
+    @property
  273
+    def app_dict(self):
  274
+        language_code = get_language()
  275
+        if language_code not in self._app_dict:
241 276
             self._populate()
242  
-        return self._app_dict
243  
-    app_dict = property(_get_app_dict)
  277
+        return self._app_dict[language_code]
244 278
 
245 279
     def resolve(self, path):
246 280
         tried = []
@@ -267,22 +301,22 @@ def resolve(self, path):
267 301
             raise Resolver404({'tried': tried, 'path': new_path})
268 302
         raise Resolver404({'path' : path})
269 303
 
270  
-    def _get_urlconf_module(self):
  304
+    @property
  305
+    def urlconf_module(self):
271 306
         try:
272 307
             return self._urlconf_module
273 308
         except AttributeError:
274 309
             self._urlconf_module = import_module(self.urlconf_name)
275 310
             return self._urlconf_module
276  
-    urlconf_module = property(_get_urlconf_module)
277 311
 
278  
-    def _get_url_patterns(self):
  312
+    @property
  313
+    def url_patterns(self):
279 314
         patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
280 315
         try:
281 316
             iter(patterns)
282 317
         except TypeError:
283 318
             raise ImproperlyConfigured("The included urlconf %s doesn't have any patterns in it" % self.urlconf_name)
284 319
         return patterns
285  
-    url_patterns = property(_get_url_patterns)
286 320
 
287 321
     def _resolve_special(self, view_type):
288 322
         callback = getattr(self.urlconf_module, 'handler%s' % view_type, None)
@@ -343,6 +377,25 @@ def reverse(self, lookup_view, *args, **kwargs):
343 377
         raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
344 378
                 "arguments '%s' not found." % (lookup_view_s, args, kwargs))
345 379
 
  380
+class LocaleRegexURLResolver(RegexURLResolver):
  381
+    """
  382
+    A URL resolver that always matches the active language code as URL prefix.
  383
+
  384
+    Rather than taking a regex argument, we just override the ``regex``
  385
+    function to always return the active language-code as regex.
  386
+    """
  387
+    def __init__(self, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
  388
+        super(LocaleRegexURLResolver, self).__init__(
  389
+            None, urlconf_name, default_kwargs, app_name, namespace)
  390
+
  391
+    @property
  392
+    def regex(self):
  393
+        language_code = get_language()
  394
+        if language_code not in self._regex_dict:
  395
+            regex_compiled = re.compile('^%s/' % language_code, re.UNICODE)
  396
+            self._regex_dict[language_code] = regex_compiled
  397
+        return self._regex_dict[language_code]
  398
+
346 399
 def resolve(path, urlconf=None):
347 400
     if urlconf is None:
348 401
         urlconf = get_urlconf()
26  django/middleware/locale.py
... ...
@@ -1,5 +1,7 @@
1  
-"this is the locale selecting middleware that will look at accept headers"
  1
+"This is the locale selecting middleware that will look at accept headers"
2 2
 
  3
+from django.core.urlresolvers import get_resolver, LocaleRegexURLResolver
  4
+from django.http import HttpResponseRedirect
3 5
 from django.utils.cache import patch_vary_headers
4 6
 from django.utils import translation
5 7
 
@@ -18,8 +20,26 @@ def process_request(self, request):
18 20
         request.LANGUAGE_CODE = translation.get_language()
19 21
 
20 22
     def process_response(self, request, response):
  23
+        language = translation.get_language()
  24
+        translation.deactivate()
  25
+
  26
+        if (response.status_code == 404 and
  27
+                not translation.get_language_from_path(request.path_info)
  28
+                    and self.is_language_prefix_patterns_used()):
  29
+            return HttpResponseRedirect(
  30
+                '/%s%s' % (language, request.get_full_path()))
  31
+
21 32
         patch_vary_headers(response, ('Accept-Language',))
22 33
         if 'Content-Language' not in response:
23  
-            response['Content-Language'] = translation.get_language()
24  
-        translation.deactivate()
  34
+            response['Content-Language'] = language
25 35
         return response
  36
+
  37
+    def is_language_prefix_patterns_used(self):
  38
+        """
  39
+        Returns `True` if the `LocaleRegexURLResolver` is used
  40
+        at root level of the urlpatterns, else it returns `False`.
  41
+        """
  42
+        for url_pattern in get_resolver(None).url_patterns:
  43
+            if isinstance(url_pattern, LocaleRegexURLResolver):
  44
+                return True
  45
+        return False
3  django/utils/translation/__init__.py
@@ -144,6 +144,9 @@ def to_locale(language):
144 144
 def get_language_from_request(request):
145 145
     return _trans.get_language_from_request(request)
146 146
 
  147
+def get_language_from_path(path):
  148
+    return _trans.get_language_from_path(path)
  149
+
147 150
 def templatize(src, origin=None):
148 151
     return _trans.templatize(src, origin)
149 152
 
4  django/utils/translation/trans_null.py
@@ -58,3 +58,7 @@ def to_locale(language):
58 58
 
59 59
 def get_language_from_request(request):
60 60
     return settings.LANGUAGE_CODE
  61
+
  62
+def get_language_from_path(request):
  63
+    return None
  64
+
24  django/utils/translation/trans_real.py
@@ -35,6 +35,8 @@
35 35
         (?:\s*,\s*|$)                            # Multiple accepts per header.
36 36
         ''', re.VERBOSE)
37 37
 
  38
+language_code_prefix_re = re.compile(r'^/([\w-]+)/')
  39
+
38 40
 def to_locale(language, to_lower=False):
39 41
     """
40 42
     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):
336 338
     """
337 339
     Checks whether there is a global language file for the given language
338 340
     code. This is used to decide whether a user-provided language is
339  
-    available. This is only used for language codes from either the cookies or
340  
-    session and during format localization.
  341
+    available. This is only used for language codes from either the cookies
  342
+    or session and during format localization.
341 343
     """
342 344
     for path in all_locale_paths():
343 345
         if gettext_module.find('django', path, [to_locale(lang_code)]) is not None:
344 346
             return True
345 347
     return False
346 348
 
  349
+def get_language_from_path(path, supported=None):
  350
+    """
  351
+    Returns the language-code if there is a valid language-code
  352
+    found in the `path`.
  353
+    """
  354
+    if supported is None:
  355
+        from django.conf import settings
  356
+        supported = dict(settings.LANGUAGES)
  357
+    regex_match = language_code_prefix_re.match(path)
  358
+    if regex_match:
  359
+        lang_code = regex_match.group(1)
  360
+        if lang_code in supported and check_for_language(lang_code):
  361
+            return lang_code
  362
+
347 363
 def get_language_from_request(request):
348 364
     """
349 365
     Analyzes the request to find what language the user wants the system to
@@ -355,6 +371,10 @@ def get_language_from_request(request):
355 371
     from django.conf import settings
356 372
     supported = dict(settings.LANGUAGES)
357 373
 
  374
+    lang_code = get_language_from_path(request.path_info, supported)
  375
+    if lang_code is not None:
  376
+        return lang_code
  377
+
358 378
     if hasattr(request, 'session'):
359 379
         lang_code = request.session.get('django_language', None)
360 380
         if lang_code in supported and lang_code is not None and check_for_language(lang_code):
10  docs/releases/1.4.txt
@@ -167,6 +167,16 @@ a :class:`~django.forms.fields.GenericIPAddressField` form field and
167 167
 the validators :data:`~django.core.validators.validate_ipv46_address` and
168 168
 :data:`~django.core.validators.validate_ipv6_address`
169 169
 
  170
+Translating URL patterns
  171
+~~~~~~~~~~~~~~~~~~~~~~~~
  172
+
  173
+Django 1.4 gained the ability to look for a language prefix in the URL pattern
  174
+when using the new :func:`django.conf.urls.i18n.i18n_patterns` helper function.
  175
+Additionally, it's now possible to define translatable URL patterns using
  176
+:func:`~django.utils.translation.ugettext_lazy`. See
  177
+:ref:`url-internationalization` for more information about the language prefix
  178
+and how to internationalize URL patterns.
  179
+
170 180
 Minor features
171 181
 ~~~~~~~~~~~~~~
172 182
 
15  docs/topics/i18n/deployment.txt
@@ -59,7 +59,9 @@ matters, you should follow these guidelines:
59 59
 
60 60
     * Make sure it's one of the first middlewares installed.
61 61
     * It should come after ``SessionMiddleware``, because ``LocaleMiddleware``
62  
-      makes use of session data.
  62
+      makes use of session data. And it should come before ``CommonMiddleware``
  63
+      because ``CommonMiddleware`` needs an activated language in order
  64
+      to resolve the requested URL.
63 65
     * If you use ``CacheMiddleware``, put ``LocaleMiddleware`` after it.
64 66
 
65 67
 For example, your :setting:`MIDDLEWARE_CLASSES` might look like this::
@@ -76,8 +78,15 @@ For example, your :setting:`MIDDLEWARE_CLASSES` might look like this::
76 78
 ``LocaleMiddleware`` tries to determine the user's language preference by
77 79
 following this algorithm:
78 80
 
79  
-    * First, it looks for a ``django_language`` key in the current user's
80  
-      session.
  81
+.. versionchanged:: 1.4
  82
+
  83
+    * First, it looks for the language prefix in the requested URL.  This is
  84
+      only performed when you are using the ``i18n_patterns`` function in your
  85
+      root URLconf. See :ref:`url-internationalization` for more information
  86
+      about the language prefix and how to internationalize URL patterns.
  87
+
  88
+    * Failing that, it looks for a ``django_language`` key in the current
  89
+      user's session.
81 90
 
82 91
     * Failing that, it looks for a cookie.
83 92
 
132  docs/topics/i18n/internationalization.txt
@@ -753,6 +753,138 @@ This isn't as fast as string interpolation in Python, so keep it to those
753 753
 cases where you really need it (for example, in conjunction with ``ngettext``
754 754
 to produce proper pluralizations).
755 755
 
  756
+.. _url-internationalization:
  757
+
  758
+Specifying translation strings: In URL patterns
  759
+===============================================
  760
+
  761
+..  versionadded:: 1.4
  762
+
  763
+.. module:: django.conf.urls.i18n
  764
+
  765
+Django provides two mechanisms to internationalize URL patterns:
  766
+
  767
+* Adding the language prefix to the root of the URL patterns to make it
  768
+  possible for :class:`~django.middleware.locale.LocaleMiddleware` to detect
  769
+  the language to activate from the requested URL.
  770
+
  771
+* Making URL patterns themselves translatable via the
  772
+  :func:`django.utils.translation.ugettext_lazy()` function.
  773
+
  774
+.. warning::
  775
+
  776
+    Using either one of these features requires that an active language be set
  777
+    for each request; in other words, you need to have
  778
+    :class:`django.middleware.locale.LocaleMiddleware` in your
  779
+    :setting:`MIDDLEWARE_CLASSES` setting.
  780
+
  781
+Language prefix in URL patterns
  782
+-------------------------------
  783
+
  784
+.. function:: i18n_patterns(prefix, pattern_description, ...)
  785
+
  786
+This function can be used in your root URLconf as a replacement for the normal
  787
+:func:`django.conf.urls.defaults.patterns` function. Django will automatically
  788
+prepend the current active language code to all url patterns defined within
  789
+:func:`~django.conf.urls.i18n.i18n_patterns`. Example URL patterns::
  790
+
  791
+    from django.conf.urls.defaults import patterns, include, url
  792
+    from django.conf.urls.i18n import i18n_patterns
  793
+
  794
+    urlpatterns = patterns(''
  795
+        url(r'^sitemap\.xml$', 'sitemap.view', name='sitemap_xml'),
  796
+    )
  797
+
  798
+    news_patterns = patterns(''
  799
+        url(r'^$', 'news.views.index', name='index'),
  800
+        url(r'^category/(?P<slug>[\w-]+)/$', 'news.views.category', name='category'),
  801
+        url(r'^(?P<slug>[\w-]+)/$', 'news.views.details', name='detail'),
  802
+    )
  803
+
  804
+    urlpatterns += i18n_patterns('',
  805
+        url(r'^about/$', 'about.view', name='about'),
  806
+        url(r'^news/$', include(news_patterns, namespace='news')),
  807
+    )
  808
+
  809
+
  810
+After defining these URL patterns, Django will automatically add the
  811
+language prefix to the URL patterns that were added by the ``i18n_patterns``
  812
+function. Example::
  813
+
  814
+    from django.core.urlresolvers import reverse
  815
+    from django.utils.translation import activate
  816
+
  817
+    >>> activate('en')
  818
+    >>> reverse('sitemap_xml')
  819
+    '/sitemap.xml'
  820
+    >>> reverse('news:index')
  821
+    '/en/news/'
  822
+
  823
+    >>> activate('nl')
  824
+    >>> reverse('news:detail', kwargs={'slug': 'news-slug'})
  825
+    '/nl/news/news-slug/'
  826
+
  827
+.. warning::
  828
+
  829
+    :func:`~django.conf.urls.i18n.i18n_patterns` is only allowed in your root
  830
+    URLconf. Using it within an included URLconf will throw an
  831
+    :exc:`ImproperlyConfigured` exception.
  832
+
  833
+.. warning::
  834
+
  835
+    Ensure that you don't have non-prefixed URL patterns that might collide
  836
+    with an automatically-added language prefix.
  837
+
  838
+
  839
+Translating URL patterns
  840
+------------------------
  841
+
  842
+URL patterns can also be marked translatable using the
  843
+:func:`~django.utils.translation.ugettext_lazy` function. Example::
  844
+
  845
+    from django.conf.urls.defaults import patterns, include, url
  846
+    from django.conf.urls.i18n import i18n_patterns
  847
+    from django.utils.translation import ugettext_lazy as _
  848
+
  849
+    urlpatterns = patterns(''
  850
+        url(r'^sitemap\.xml$', 'sitemap.view', name='sitemap_xml'),
  851
+    )
  852
+
  853
+    news_patterns = patterns(''
  854
+        url(r'^$', 'news.views.index', name='index'),
  855
+        url(_(r'^category/(?P<slug>[\w-]+)/$'), 'news.views.category', name='category'),
  856
+        url(r'^(?P<slug>[\w-]+)/$', 'news.views.details', name='detail'),
  857
+    )
  858
+
  859
+    urlpatterns += i18n_patterns('',
  860
+        url(_(r'^about/$'), 'about.view', name='about'),
  861
+        url(_(r'^news/$'), include(news_patterns, namespace='news')),
  862
+    )
  863
+
  864
+
  865
+After you've created the translations (see :doc:`localization` for more
  866
+information), the :func:`~django.core.urlresolvers.reverse` function will
  867
+return the URL in the active language. Example::
  868
+
  869
+    from django.core.urlresolvers import reverse
  870
+    from django.utils.translation import activate
  871
+
  872
+    >>> activate('en')
  873
+    >>> reverse('news:category', kwargs={'slug': 'recent'})
  874
+    '/en/news/category/recent/'
  875
+
  876
+    >>> activate('nl')
  877
+    >>> reverse('news:category', kwargs={'slug': 'recent'})
  878
+    '/nl/nieuws/categorie/recent/'
  879
+
  880
+.. warning::
  881
+
  882
+    In most cases, it's best to use translated URLs only within a
  883
+    language-code-prefixed block of patterns (using
  884
+    :func:`~django.conf.urls.i18n.i18n_patterns`), to avoid the possibility
  885
+    that a carelessly translated URL causes a collision with a non-translated
  886
+    URL pattern.
  887
+
756 888
 .. _set_language-redirect-view:
757 889
 
758 890
 The ``set_language`` redirect view
0  tests/regressiontests/i18n/patterns/__init__.py
No changes.
BIN  tests/regressiontests/i18n/patterns/locale/en/LC_MESSAGES/django.mo
Binary file not shown
37  tests/regressiontests/i18n/patterns/locale/en/LC_MESSAGES/django.po
... ...
@@ -0,0 +1,37 @@
  1
+# SOME DESCRIPTIVE TITLE.
  2
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
  3
+# This file is distributed under the same license as the PACKAGE package.
  4
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
  5
+#
  6
+msgid ""
  7
+msgstr ""
  8
+"Project-Id-Version: PACKAGE VERSION\n"
  9
+"Report-Msgid-Bugs-To: \n"
  10
+"POT-Creation-Date: 2011-06-15 11:33+0200\n"
  11
+"PO-Revision-Date: 2011-06-14 16:16+0100\n"
  12
+"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
  13
+"Language-Team: LANGUAGE <LL@li.org>\n"
  14
+"MIME-Version: 1.0\n"
  15
+"Content-Type: text/plain; charset=UTF-8\n"
  16
+"Content-Transfer-Encoding: 8bit\n"
  17
+"Language: \n"
  18
+
  19
+#: urls/default.py:11
  20
+msgid "^translated/$"
  21
+msgstr "^translated/$"
  22
+
  23
+#: urls/default.py:12
  24
+msgid "^translated/(?P<slug>[\\w-]+)/$"
  25
+msgstr "^translated/(?P<slug>[\\w-]+)/$"
  26
+
  27
+#: urls/default.py:17
  28
+msgid "^users/$"
  29
+msgstr "^users/$"
  30
+
  31
+#: urls/default.py:18 urls/wrong.py:7
  32
+msgid "^account/"
  33
+msgstr "^account/"
  34
+
  35
+#: urls/namespace.py:9 urls/wrong_namespace.py:10
  36
+msgid "^register/$"
  37
+msgstr "^register/$"
BIN  tests/regressiontests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo
Binary file not shown
38  tests/regressiontests/i18n/patterns/locale/nl/LC_MESSAGES/django.po
... ...
@@ -0,0 +1,38 @@
  1
+# SOME DESCRIPTIVE TITLE.
  2
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
  3
+# This file is distributed under the same license as the PACKAGE package.
  4
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
  5
+#
  6
+msgid ""
  7
+msgstr ""
  8
+"Project-Id-Version: PACKAGE VERSION\n"
  9
+"Report-Msgid-Bugs-To: \n"
  10
+"POT-Creation-Date: 2011-06-15 11:33+0200\n"
  11
+"PO-Revision-Date: 2011-06-14 16:16+0100\n"
  12
+"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
  13
+"Language-Team: LANGUAGE <LL@li.org>\n"
  14
+"MIME-Version: 1.0\n"
  15
+"Content-Type: text/plain; charset=UTF-8\n"
  16
+"Content-Transfer-Encoding: 8bit\n"
  17
+"Language: \n"
  18
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
  19
+
  20
+#: urls/default.py:11
  21
+msgid "^translated/$"
  22
+msgstr "^vertaald/$"
  23
+
  24
+#: urls/default.py:12
  25
+msgid "^translated/(?P<slug>[\\w-]+)/$"
  26
+msgstr "^vertaald/(?P<slug>[\\w-]+)/$"
  27
+
  28
+#: urls/default.py:17
  29
+msgid "^users/$"
  30
+msgstr "^gebruikers/$"
  31
+
  32
+#: urls/default.py:18 urls/wrong.py:7
  33
+msgid "^account/"
  34
+msgstr "^profiel/"
  35
+
  36
+#: urls/namespace.py:9 urls/wrong_namespace.py:10
  37
+msgid "^register/$"
  38
+msgstr "^registeren/$"
BIN  tests/regressiontests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.mo
Binary file not shown
38  tests/regressiontests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.po
... ...
@@ -0,0 +1,38 @@
  1
+# SOME DESCRIPTIVE TITLE.
  2
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
  3
+# This file is distributed under the same license as the PACKAGE package.
  4
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
  5
+#
  6
+msgid ""
  7
+msgstr ""
  8
+"Project-Id-Version: PACKAGE VERSION\n"
  9
+"Report-Msgid-Bugs-To: \n"
  10
+"POT-Creation-Date: 2011-06-15 11:34+0200\n"
  11
+"PO-Revision-Date: 2011-06-14 16:17+0100\n"
  12
+"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
  13
+"Language-Team: LANGUAGE <LL@li.org>\n"
  14
+"MIME-Version: 1.0\n"
  15
+"Content-Type: text/plain; charset=UTF-8\n"
  16
+"Content-Transfer-Encoding: 8bit\n"
  17
+"Language: \n"
  18
+"Plural-Forms: nplurals=2; plural=(n > 1)\n"
  19
+
  20
+#: urls/default.py:11
  21
+msgid "^translated/$"
  22
+msgstr "^traduzidos/$"
  23
+
  24
+#: urls/default.py:12
  25
+msgid "^translated/(?P<slug>[\\w-]+)/$"
  26
+msgstr "^traduzidos/(?P<slug>[\\w-]+)/$"
  27
+
  28
+#: urls/default.py:17
  29
+msgid "^users/$"
  30
+msgstr "^usuarios/$"
  31
+
  32
+#: urls/default.py:18 urls/wrong.py:7
  33
+msgid "^account/"
  34
+msgstr "^conta/"
  35
+
  36
+#: urls/namespace.py:9 urls/wrong_namespace.py:10
  37
+msgid "^register/$"
  38
+msgstr "^registre-se/$"
0  tests/regressiontests/i18n/patterns/templates/404.html
No changes.
0  tests/regressiontests/i18n/patterns/templates/dummy.html
No changes.
241  tests/regressiontests/i18n/patterns/tests.py
... ...
@@ -0,0 +1,241 @@
  1
+import os
  2
+
  3
+from django.core.exceptions import ImproperlyConfigured
  4
+from django.core.urlresolvers import reverse, clear_url_caches
  5
+from django.test import TestCase
  6
+from django.test.utils import override_settings
  7
+from django.utils import translation
  8
+
  9
+
  10
+class URLTestCaseBase(TestCase):
  11
+    """
  12
+    TestCase base-class for the URL tests.
  13
+    """
  14
+    urls = 'regressiontests.i18n.patterns.urls.default'
  15
+
  16
+    def setUp(self):
  17
+        # Make sure the cache is empty before we are doing our tests.
  18
+        clear_url_caches()
  19
+
  20
+    def tearDown(self):
  21
+        # Make sure we will leave an empty cache for other testcases.
  22
+        clear_url_caches()
  23
+
  24
+URLTestCaseBase = override_settings(
  25
+    USE_I18N=True,
  26
+    LOCALE_PATHS=(
  27
+        os.path.join(os.path.dirname(__file__), 'locale'),
  28
+    ),
  29
+    TEMPLATE_DIRS=(
  30
+        os.path.join(os.path.dirname(__file__), 'templates'),
  31
+    ),
  32
+    LANGUAGE_CODE='en',
  33
+    LANGUAGES=(
  34
+        ('nl', 'Dutch'),
  35
+        ('en', 'English'),
  36
+        ('pt-br', 'Brazilian Portuguese'),
  37
+    ),
  38
+    MIDDLEWARE_CLASSES=(
  39
+        'django.middleware.locale.LocaleMiddleware',
  40
+        'django.middleware.common.CommonMiddleware',
  41
+    ),
  42
+)(URLTestCaseBase)
  43
+
  44
+
  45
+class URLPrefixTests(URLTestCaseBase):
  46
+    """
  47
+    Tests if the `i18n_patterns` is adding the prefix correctly.
  48
+    """
  49
+    def test_not_prefixed(self):
  50
+        with translation.override('en'):
  51
+            self.assertEqual(reverse('not-prefixed'), '/not-prefixed/')
  52
+        with translation.override('nl'):
  53
+            self.assertEqual(reverse('not-prefixed'), '/not-prefixed/')
  54
+
  55
+    def test_prefixed(self):
  56
+        with translation.override('en'):
  57
+            self.assertEqual(reverse('prefixed'), '/en/prefixed/')
  58
+        with translation.override('nl'):
  59
+            self.assertEqual(reverse('prefixed'), '/nl/prefixed/')
  60
+
  61
+    @override_settings(ROOT_URLCONF='regressiontests.i18n.patterns.urls.wrong')
  62
+    def test_invalid_prefix_use(self):
  63
+        self.assertRaises(ImproperlyConfigured, lambda: reverse('account:register'))
  64
+
  65
+
  66
+class URLDisabledTests(URLTestCaseBase):
  67
+    urls = 'regressiontests.i18n.patterns.urls.disabled'
  68
+
  69
+    @override_settings(USE_I18N=False)
  70
+    def test_prefixed_i18n_disabled(self):
  71
+        with translation.override('en'):
  72
+            self.assertEqual(reverse('prefixed'), '/prefixed/')
  73
+        with translation.override('nl'):
  74
+            self.assertEqual(reverse('prefixed'), '/prefixed/')
  75
+
  76
+
  77
+class URLTranslationTests(URLTestCaseBase):
  78
+    """
  79
+    Tests if the pattern-strings are translated correctly (within the
  80
+    `i18n_patterns` and the normal `patterns` function).
  81
+    """
  82
+    def test_no_prefix_translated(self):
  83
+        with translation.override('en'):
  84
+            self.assertEqual(reverse('no-prefix-translated'), '/translated/')
  85
+            self.assertEqual(reverse('no-prefix-translated-slug', kwargs={'slug': 'yeah'}), '/translated/yeah/')
  86
+
  87
+        with translation.override('nl'):
  88
+            self.assertEqual(reverse('no-prefix-translated'), '/vertaald/')
  89
+            self.assertEqual(reverse('no-prefix-translated-slug', kwargs={'slug': 'yeah'}), '/vertaald/yeah/')
  90
+
  91
+        with translation.override('pt-br'):
  92
+            self.assertEqual(reverse('no-prefix-translated'), '/traduzidos/')
  93
+            self.assertEqual(reverse('no-prefix-translated-slug', kwargs={'slug': 'yeah'}), '/traduzidos/yeah/')
  94
+
  95
+    def test_users_url(self):
  96
+        with translation.override('en'):
  97
+            self.assertEqual(reverse('users'), '/en/users/')
  98
+
  99
+        with translation.override('nl'):
  100
+            self.assertEqual(reverse('users'), '/nl/gebruikers/')
  101
+
  102
+        with translation.override('pt-br'):
  103
+            self.assertEqual(reverse('users'), '/pt-br/usuarios/')
  104
+
  105
+
  106
+class URLNamespaceTests(URLTestCaseBase):
  107
+    """
  108
+    Tests if the translations are still working within namespaces.
  109
+    """
  110
+    def test_account_register(self):
  111
+        with translation.override('en'):
  112
+            self.assertEqual(reverse('account:register'), '/en/account/register/')
  113
+
  114
+        with translation.override('nl'):
  115
+            self.assertEqual(reverse('account:register'), '/nl/profiel/registeren/')
  116
+
  117
+
  118
+class URLRedirectTests(URLTestCaseBase):
  119
+    """
  120
+    Tests if the user gets redirected to the right URL when there is no
  121
+    language-prefix in the request URL.
  122
+    """
  123
+    def test_no_prefix_response(self):
  124
+        response = self.client.get('/not-prefixed/')
  125
+        self.assertEqual(response.status_code, 200)
  126
+
  127
+    def test_en_redirect(self):
  128
+        response = self.client.get('/account/register/', HTTP_ACCEPT_LANGUAGE='en')
  129
+        self.assertRedirects(response, 'http://testserver/en/account/register/')
  130
+
  131
+        response = self.client.get(response['location'])
  132
+        self.assertEqual(response.status_code, 200)
  133
+
  134
+    def test_en_redirect_wrong_url(self):
  135
+        response = self.client.get('/profiel/registeren/', HTTP_ACCEPT_LANGUAGE='en')
  136
+        self.assertEqual(response.status_code, 302)
  137
+        self.assertEqual(response['location'], 'http://testserver/en/profiel/registeren/')
  138
+
  139
+        response = self.client.get(response['location'])
  140
+        self.assertEqual(response.status_code, 404)
  141
+
  142
+    def test_nl_redirect(self):
  143
+        response = self.client.get('/profiel/registeren/', HTTP_ACCEPT_LANGUAGE='nl')
  144
+        self.assertRedirects(response, 'http://testserver/nl/profiel/registeren/')
  145
+
  146
+        response = self.client.get(response['location'])
  147
+        self.assertEqual(response.status_code, 200)
  148
+
  149
+    def test_nl_redirect_wrong_url(self):
  150
+        response = self.client.get('/account/register/', HTTP_ACCEPT_LANGUAGE='nl')
  151
+        self.assertEqual(response.status_code, 302)
  152
+        self.assertEqual(response['location'], 'http://testserver/nl/account/register/')
  153
+
  154
+        response = self.client.get(response['location'])
  155
+        self.assertEqual(response.status_code, 404)
  156
+
  157
+    def test_pt_br_redirect(self):
  158
+        response = self.client.get('/conta/registre-se/', HTTP_ACCEPT_LANGUAGE='pt-br')
  159
+        self.assertRedirects(response, 'http://testserver/pt-br/conta/registre-se/')
  160
+
  161
+        response = self.client.get(response['location'])
  162
+        self.assertEqual(response.status_code, 200)
  163
+
  164
+
  165
+class URLRedirectWithoutTrailingSlashTests(URLTestCaseBase):
  166
+    """
  167
+    Tests the redirect when the requested URL doesn't end with a slash
  168
+    (`settings.APPEND_SLASH=True`).
  169
+    """
  170
+    def test_not_prefixed_redirect(self):
  171
+        response = self.client.get('/not-prefixed', HTTP_ACCEPT_LANGUAGE='en')
  172
+        self.assertEqual(response.status_code, 301)
  173
+        self.assertEqual(response['location'], 'http://testserver/not-prefixed/')
  174
+
  175
+    def test_en_redirect(self):
  176
+        response = self.client.get('/account/register', HTTP_ACCEPT_LANGUAGE='en')
  177
+        self.assertEqual(response.status_code, 302)
  178
+        self.assertEqual(response['location'], 'http://testserver/en/account/register')
  179
+
  180
+        response = self.client.get(response['location'])
  181
+        self.assertEqual(response.status_code, 301)
  182
+        self.assertEqual(response['location'], 'http://testserver/en/account/register/')
  183
+
  184
+
  185
+class URLRedirectWithoutTrailingSlashSettingTests(URLTestCaseBase):
  186
+    """
  187
+    Tests the redirect when the requested URL doesn't end with a slash
  188
+    (`settings.APPEND_SLASH=False`).
  189
+    """
  190
+    @override_settings(APPEND_SLASH=False)
  191
+    def test_not_prefixed_redirect(self):
  192
+        response = self.client.get('/not-prefixed', HTTP_ACCEPT_LANGUAGE='en')
  193
+        self.assertEqual(response.status_code, 302)
  194
+        self.assertEqual(response['location'], 'http://testserver/en/not-prefixed')
  195
+
  196
+        response = self.client.get(response['location'])
  197
+        self.assertEqual(response.status_code, 404)
  198
+
  199
+    @override_settings(APPEND_SLASH=False)
  200
+    def test_en_redirect(self):
  201
+        response = self.client.get('/account/register', HTTP_ACCEPT_LANGUAGE='en')
  202
+        self.assertEqual(response.status_code, 302)
  203
+        self.assertEqual(response['location'], 'http://testserver/en/account/register')
  204
+
  205
+        response = self.client.get(response['location'])
  206
+        self.assertEqual(response.status_code, 404)
  207
+
  208
+
  209
+class URLResponseTests(URLTestCaseBase):
  210
+    """
  211
+    Tests if the response has the right language-code.
  212
+    """
  213
+    def test_not_prefixed_with_prefix(self):
  214
+        response = self.client.get('/en/not-prefixed/')
  215
+        self.assertEqual(response.status_code, 404)
  216
+
  217
+    def test_en_url(self):
  218
+        response = self.client.get('/en/account/register/')
  219
+        self.assertEqual(response.status_code, 200)
  220
+        self.assertEqual(response['content-language'], 'en')
  221
+        self.assertEqual(response.context['LANGUAGE_CODE'], 'en')
  222
+
  223
+    def test_nl_url(self):
  224
+        response = self.client.get('/nl/profiel/registeren/')
  225
+        self.assertEqual(response.status_code, 200)
  226
+        self.assertEqual(response['content-language'], 'nl')
  227
+        self.assertEqual(response.context['LANGUAGE_CODE'], 'nl')
  228
+
  229
+    def test_wrong_en_prefix(self):
  230
+        response = self.client.get('/en/profiel/registeren/')
  231
+        self.assertEqual(response.status_code, 404)
  232
+
  233
+    def test_wrong_nl_prefix(self):
  234
+        response = self.client.get('/nl/account/register/')
  235
+        self.assertEqual(response.status_code, 404)
  236
+
  237
+    def test_pt_br_url(self):
  238
+        response = self.client.get('/pt-br/conta/registre-se/')
  239
+        self.assertEqual(response.status_code, 200)
  240
+        self.assertEqual(response['content-language'], 'pt-br')
  241
+        self.assertEqual(response.context['LANGUAGE_CODE'], 'pt-br')
0  tests/regressiontests/i18n/patterns/urls/__init__.py
No changes.
19  tests/regressiontests/i18n/patterns/urls/default.py
... ...
@@ -0,0 +1,19 @@
  1
+from django.conf.urls.defaults import patterns, include, url
  2
+from django.conf.urls.i18n import i18n_patterns
  3
+from django.utils.translation import ugettext_lazy as _
  4
+from django.views.generic import TemplateView
  5
+
  6
+
  7
+view = TemplateView.as_view(template_name='dummy.html')
  8
+
  9
+urlpatterns = patterns('',
  10
+    url(r'^not-prefixed/$', view, name='not-prefixed'),
  11
+    url(_(r'^translated/$'), view, name='no-prefix-translated'),
  12
+    url(_(r'^translated/(?P<slug>[\w-]+)/$'), view, name='no-prefix-translated-slug'),
  13
+)
  14
+
  15
+urlpatterns += i18n_patterns('',
  16
+    url(r'^prefixed/$', view, name='prefixed'),
  17
+    url(_(r'^users/$'), view, name='users'),
  18
+    url(_(r'^account/'), include('regressiontests.i18n.patterns.urls.namespace', namespace='account')),
  19
+)
9  tests/regressiontests/i18n/patterns/urls/disabled.py
... ...
@@ -0,0 +1,9 @@
  1
+from django.conf.urls.defaults import url
  2
+from django.conf.urls.i18n import i18n_patterns
  3
+from django.views.generic import TemplateView
  4
+
  5
+view = TemplateView.as_view(template_name='dummy.html')
  6
+
  7
+urlpatterns = i18n_patterns('',
  8
+    url(r'^prefixed/$', view, name='prefixed'),
  9
+)
10  tests/regressiontests/i18n/patterns/urls/namespace.py
... ...
@@ -0,0 +1,10 @@
  1
+from django.conf.urls.defaults import patterns, include, url
  2
+from django.utils.translation import ugettext_lazy as _
  3
+from django.views.generic import TemplateView
  4
+
  5
+
  6
+view = TemplateView.as_view(template_name='dummy.html')
  7
+
  8
+urlpatterns = patterns('',
  9
+    url(_(r'^register/$'), view, name='register'),
  10
+)
8  tests/regressiontests/i18n/patterns/urls/wrong.py
... ...
@@ -0,0 +1,8 @@
  1
+from django.conf.urls.defaults import patterns, include, url
  2
+from django.conf.urls.i18n import i18n_patterns
  3
+from django.utils.translation import ugettext_lazy as _
  4
+
  5
+
  6
+urlpatterns = i18n_patterns('',
  7
+    url(_(r'^account/'), include('regressiontests.i18n.patterns.urls.wrong_namespace', namespace='account')),
  8
+)
11  tests/regressiontests/i18n/patterns/urls/wrong_namespace.py
... ...
@@ -0,0 +1,11 @@
  1
+from django.conf.urls.defaults import include, url
  2
+from django.conf.urls.i18n import i18n_patterns
  3
+from django.utils.translation import ugettext_lazy as _
  4
+from django.views.generic import TemplateView
  5
+
  6
+
  7
+view = TemplateView.as_view(template_name='dummy.html')
  8
+
  9
+urlpatterns = i18n_patterns('',
  10
+    url(_(r'^register/$'), view, name='register'),
  11
+)
19  tests/regressiontests/i18n/tests.py
@@ -3,13 +3,12 @@
3 3
 import datetime
4 4
 import decimal
5 5
 import os
6  
-import sys
7 6
 import pickle
8 7
 from threading import local
9 8
 
10 9
 from django.conf import settings
11 10
 from django.template import Template, Context
12  
-from django.test import TestCase
  11
+from django.test import TestCase, RequestFactory
13 12
 from django.utils.formats import (get_format, date_format, time_format,
14 13
     localize, localize_input, iter_format_modules, get_format_modules)
15 14
 from django.utils.importlib import import_module
@@ -18,14 +17,14 @@
18 17
 from django.utils import translation
19 18
 from django.utils.translation import (ugettext, ugettext_lazy, activate,
20 19
         deactivate, gettext_lazy, pgettext, npgettext, to_locale,
21  
-        get_language_info, get_language)
  20
+        get_language_info, get_language, get_language_from_request)
22 21
 
23 22
 
24 23
 from forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm
25 24
 from models import Company, TestModel
26 25
 
27 26
 from commands.tests import *
28  
-
  27
+from patterns.tests import *
29 28
 from test_warnings import DeprecationWarningTests
30 29
 
31 30
 class TranslationTests(TestCase):
@@ -494,6 +493,9 @@ def test_localize_templatetag_and_filter(self):
494 493
 
495 494
 class MiscTests(TestCase):
496 495
 
  496
+    def setUp(self):