Permalink
Browse files

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...
1 parent 62bb4b8 commit 896e3c69c7eec311085da349a329ee80c8fca132 @jezdez jezdez committed Jun 15, 2011
Showing with 751 additions and 58 deletions.
  1. +1 −0 AUTHORS
  2. +19 −1 django/conf/urls/defaults.py
  3. +16 −1 django/conf/urls/i18n.py
  4. +3 −3 django/contrib/admindocs/views.py
  5. +87 −34 django/core/urlresolvers.py
  6. +23 −3 django/middleware/locale.py
  7. +3 −0 django/utils/translation/__init__.py
  8. +4 −0 django/utils/translation/trans_null.py
  9. +22 −2 django/utils/translation/trans_real.py
  10. +10 −0 docs/releases/1.4.txt
  11. +12 −3 docs/topics/i18n/deployment.txt
  12. +132 −0 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 −0 tests/regressiontests/i18n/patterns/locale/en/LC_MESSAGES/django.po
  16. BIN tests/regressiontests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo
  17. +38 −0 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 −0 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 −0 tests/regressiontests/i18n/patterns/tests.py
  23. 0 tests/regressiontests/i18n/patterns/urls/__init__.py
  24. +19 −0 tests/regressiontests/i18n/patterns/urls/default.py
  25. +9 −0 tests/regressiontests/i18n/patterns/urls/disabled.py
  26. +10 −0 tests/regressiontests/i18n/patterns/urls/namespace.py
  27. +8 −0 tests/regressiontests/i18n/patterns/urls/wrong.py
  28. +11 −0 tests/regressiontests/i18n/patterns/urls/wrong_namespace.py
  29. +8 −11 tests/regressiontests/i18n/tests.py
View
@@ -94,6 +94,7 @@ answer newbie questions, and generally made Django that much better:
Sean Brant
Andrew Brehaut <http://brehaut.net/blog>
David Brenneman <http://davidbrenneman.com>
+ Orne Brocaar <http://brocaar.com/>
brut.alll@gmail.com
bthomas
btoll@bestweb.net
@@ -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.utils.importlib import import_module
+
__all__ = ['handler404', 'handler500', 'include', 'patterns', 'url']
@@ -15,6 +18,21 @@ def include(arg, namespace=None, app_name=None):
else:
# No namespace hint - use manually provided namespace
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)
def patterns(prefix, *args):
@@ -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('',
(r'^setlang/$', 'django.views.i18n.set_language'),
@@ -346,12 +346,12 @@ def extract_views_from_urlpatterns(urlpatterns, base=''):
"""
views = []
for p in urlpatterns:
- if hasattr(p, '_get_callback'):
+ if hasattr(p, 'callback'):
try:
- views.append((p._get_callback(), base + p.regex.pattern))
+ views.append((p.callback, base + p.regex.pattern))
except ViewDoesNotExist:
continue
- elif hasattr(p, '_get_url_patterns'):
+ elif hasattr(p, 'url_patterns'):
try:
patterns = p.url_patterns
except ImportError:
@@ -11,13 +11,14 @@
from threading import local
from django.http import Http404
-from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.utils.datastructures import MultiValueDict
from django.utils.encoding import iri_to_uri, force_unicode, smart_str
from django.utils.functional import memoize, lazy
from django.utils.importlib import import_module
from django.utils.regex_helper import normalize
+from django.utils.translation import get_language
+
_resolver_cache = {} # Maps URLconf modules to RegexURLResolver instances.
_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=
url_name = '.'.join([func.__module__, func.__name__])
self.url_name = url_name
+ @property
def namespace(self):
return ':'.join(self.namespaces)
- namespace = property(namespace)
+ @property
def view_name(self):
return ':'.join([ x for x in [ self.namespace, self.url_name ] if x ])
- view_name = property(view_name)
def __getitem__(self, index):
return (self.func, self.args, self.kwargs)[index]
@@ -115,13 +116,43 @@ def get_mod_func(callback):
return callback, ''
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):
- # regex is a string representing a regular expression.
+ LocaleRegexProvider.__init__(self, regex)
# 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
# callable object (view).
- self.regex = re.compile(regex, re.UNICODE)
if callable(callback):
self._callback = callback
else:
@@ -157,7 +188,8 @@ def resolve(self, path):
return ResolverMatch(self.callback, args, kwargs, self.name)
- def _get_callback(self):
+ @property
+ def callback(self):
if self._callback is not None:
return self._callback
try:
@@ -169,23 +201,21 @@ def _get_callback(self):
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)))
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):
- # regex is a string representing a regular expression.
+ LocaleRegexProvider.__init__(self, regex)
# urlconf_name is a string representing the module containing URLconfs.
- self.regex = re.compile(regex, re.UNICODE)
self.urlconf_name = urlconf_name
if not isinstance(urlconf_name, basestring):
self._urlconf_module = self.urlconf_name
self.callback = None
self.default_kwargs = default_kwargs or {}
self.namespace = namespace
self.app_name = app_name
- self._reverse_dict = None
- self._namespace_dict = None
- self._app_dict = None
+ self._reverse_dict = {}
+ self._namespace_dict = {}
+ self._app_dict = {}
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))
@@ -194,6 +224,7 @@ def _populate(self):
lookups = MultiValueDict()
namespaces = {}
apps = {}
+ language_code = get_language()
for pattern in reversed(self.url_patterns):
p_pattern = pattern.regex.pattern
if p_pattern.startswith('^'):
@@ -220,27 +251,30 @@ def _populate(self):
lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args))
if pattern.name is not None:
lookups.appendlist(pattern.name, (bits, p_pattern, pattern.default_args))
- self._reverse_dict = lookups
- self._namespace_dict = namespaces
- self._app_dict = apps
-
- def _get_reverse_dict(self):
- if self._reverse_dict is None:
+ self._reverse_dict[language_code] = lookups
+ self._namespace_dict[language_code] = namespaces
+ self._app_dict[language_code] = apps
+
+ @property
+ def reverse_dict(self):
+ language_code = get_language()
+ if language_code not in self._reverse_dict:
self._populate()
- return self._reverse_dict
- reverse_dict = property(_get_reverse_dict)
+ return self._reverse_dict[language_code]
- def _get_namespace_dict(self):
- if self._namespace_dict is None:
+ @property
+ def namespace_dict(self):
+ language_code = get_language()
+ if language_code not in self._namespace_dict:
self._populate()
- return self._namespace_dict
- namespace_dict = property(_get_namespace_dict)
+ return self._namespace_dict[language_code]
- def _get_app_dict(self):
- if self._app_dict is None:
+ @property
+ def app_dict(self):
+ language_code = get_language()
+ if language_code not in self._app_dict:
self._populate()
- return self._app_dict
- app_dict = property(_get_app_dict)
+ return self._app_dict[language_code]
def resolve(self, path):
tried = []
@@ -267,22 +301,22 @@ def resolve(self, path):
raise Resolver404({'tried': tried, 'path': new_path})
raise Resolver404({'path' : path})
- def _get_urlconf_module(self):
+ @property
+ def urlconf_module(self):
try:
return self._urlconf_module
except AttributeError:
self._urlconf_module = import_module(self.urlconf_name)
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)
try:
iter(patterns)
except TypeError:
raise ImproperlyConfigured("The included urlconf %s doesn't have any patterns in it" % self.urlconf_name)
return patterns
- url_patterns = property(_get_url_patterns)
def _resolve_special(self, view_type):
callback = getattr(self.urlconf_module, 'handler%s' % view_type, None)
@@ -343,6 +377,25 @@ def reverse(self, lookup_view, *args, **kwargs):
raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
"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):
if urlconf is None:
urlconf = get_urlconf()
@@ -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 import translation
@@ -18,8 +20,26 @@ def process_request(self, request):
request.LANGUAGE_CODE = translation.get_language()
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',))
if 'Content-Language' not in response:
- response['Content-Language'] = translation.get_language()
- translation.deactivate()
+ response['Content-Language'] = language
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
@@ -144,6 +144,9 @@ def to_locale(language):
def 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):
return _trans.templatize(src, origin)
@@ -58,3 +58,7 @@ def to_locale(language):
def get_language_from_request(request):
return settings.LANGUAGE_CODE
+
+def get_language_from_path(request):
+ return None
+
Oops, something went wrong.

0 comments on commit 896e3c6

Please sign in to comment.