Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
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...
commit 896e3c69c7eec311085da349a329ee80c8fca132 1 parent 62bb4b8
@jezdez jezdez authored
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
1  AUTHORS
@@ -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
View
20 django/conf/urls/defaults.py
@@ -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):
View
17 django/conf/urls/i18n.py
@@ -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'),
View
6 django/contrib/admindocs/views.py
@@ -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:
View
121 django/core/urlresolvers.py
@@ -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,13 +201,11 @@ 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
@@ -183,9 +213,9 @@ def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, name
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()
View
26 django/middleware/locale.py
@@ -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
View
3  django/utils/translation/__init__.py
@@ -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)
View
4 django/utils/translation/trans_null.py
@@ -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
+
View
24 django/utils/translation/trans_real.py
@@ -35,6 +35,8 @@
(?:\s*,\s*|$) # Multiple accepts per header.
''', re.VERBOSE)
+language_code_prefix_re = re.compile(r'^/([\w-]+)/')
+
def to_locale(language, to_lower=False):
"""
Turns a language name (en-us) into a locale name (en_US). If 'to_lower' is
@@ -336,14 +338,28 @@ def check_for_language(lang_code):
"""
Checks whether there is a global language file for the given language
code. This is used to decide whether a user-provided language is
- available. This is only used for language codes from either the cookies or
- session and during format localization.
+ available. This is only used for language codes from either the cookies
+ or session and during format localization.
"""
for path in all_locale_paths():
if gettext_module.find('django', path, [to_locale(lang_code)]) is not None:
return True
return False
+def get_language_from_path(path, supported=None):
+ """
+ Returns the language-code if there is a valid language-code
+ found in the `path`.
+ """
+ if supported is None:
+ from django.conf import settings
+ supported = dict(settings.LANGUAGES)
+ regex_match = language_code_prefix_re.match(path)
+ if regex_match:
+ lang_code = regex_match.group(1)
+ if lang_code in supported and check_for_language(lang_code):
+ return lang_code
+
def get_language_from_request(request):
"""
Analyzes the request to find what language the user wants the system to
@@ -355,6 +371,10 @@ def get_language_from_request(request):
from django.conf import settings
supported = dict(settings.LANGUAGES)
+ lang_code = get_language_from_path(request.path_info, supported)
+ if lang_code is not None:
+ return lang_code
+
if hasattr(request, 'session'):
lang_code = request.session.get('django_language', None)
if lang_code in supported and lang_code is not None and check_for_language(lang_code):
View
10 docs/releases/1.4.txt
@@ -167,6 +167,16 @@ a :class:`~django.forms.fields.GenericIPAddressField` form field and
the validators :data:`~django.core.validators.validate_ipv46_address` and
:data:`~django.core.validators.validate_ipv6_address`
+Translating URL patterns
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Django 1.4 gained the ability to look for a language prefix in the URL pattern
+when using the new :func:`django.conf.urls.i18n.i18n_patterns` helper function.
+Additionally, it's now possible to define translatable URL patterns using
+:func:`~django.utils.translation.ugettext_lazy`. See
+:ref:`url-internationalization` for more information about the language prefix
+and how to internationalize URL patterns.
+
Minor features
~~~~~~~~~~~~~~
View
15 docs/topics/i18n/deployment.txt
@@ -59,7 +59,9 @@ matters, you should follow these guidelines:
* Make sure it's one of the first middlewares installed.
* It should come after ``SessionMiddleware``, because ``LocaleMiddleware``
- makes use of session data.
+ makes use of session data. And it should come before ``CommonMiddleware``
+ because ``CommonMiddleware`` needs an activated language in order
+ to resolve the requested URL.
* If you use ``CacheMiddleware``, put ``LocaleMiddleware`` after it.
For example, your :setting:`MIDDLEWARE_CLASSES` might look like this::
@@ -76,8 +78,15 @@ For example, your :setting:`MIDDLEWARE_CLASSES` might look like this::
``LocaleMiddleware`` tries to determine the user's language preference by
following this algorithm:
- * First, it looks for a ``django_language`` key in the current user's
- session.
+.. versionchanged:: 1.4
+
+ * First, it looks for the language prefix in the requested URL. This is
+ only performed when you are using the ``i18n_patterns`` function in your
+ root URLconf. See :ref:`url-internationalization` for more information
+ about the language prefix and how to internationalize URL patterns.
+
+ * Failing that, it looks for a ``django_language`` key in the current
+ user's session.
* Failing that, it looks for a cookie.
View
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
cases where you really need it (for example, in conjunction with ``ngettext``
to produce proper pluralizations).
+.. _url-internationalization:
+
+Specifying translation strings: In URL patterns
+===============================================
+
+.. versionadded:: 1.4
+
+.. module:: django.conf.urls.i18n
+
+Django provides two mechanisms to internationalize URL patterns:
+
+* Adding the language prefix to the root of the URL patterns to make it
+ possible for :class:`~django.middleware.locale.LocaleMiddleware` to detect
+ the language to activate from the requested URL.
+
+* Making URL patterns themselves translatable via the
+ :func:`django.utils.translation.ugettext_lazy()` function.
+
+.. warning::
+
+ Using either one of these features requires that an active language be set
+ for each request; in other words, you need to have
+ :class:`django.middleware.locale.LocaleMiddleware` in your
+ :setting:`MIDDLEWARE_CLASSES` setting.
+
+Language prefix in URL patterns
+-------------------------------
+
+.. function:: i18n_patterns(prefix, pattern_description, ...)
+
+This function can be used in your root URLconf as a replacement for the normal
+:func:`django.conf.urls.defaults.patterns` function. Django will automatically
+prepend the current active language code to all url patterns defined within
+:func:`~django.conf.urls.i18n.i18n_patterns`. Example URL patterns::
+
+ from django.conf.urls.defaults import patterns, include, url
+ from django.conf.urls.i18n import i18n_patterns
+
+ urlpatterns = patterns(''
+ url(r'^sitemap\.xml$', 'sitemap.view', name='sitemap_xml'),
+ )
+
+ news_patterns = patterns(''
+ url(r'^$', 'news.views.index', name='index'),
+ url(r'^category/(?P<slug>[\w-]+)/$', 'news.views.category', name='category'),
+ url(r'^(?P<slug>[\w-]+)/$', 'news.views.details', name='detail'),
+ )
+
+ urlpatterns += i18n_patterns('',
+ url(r'^about/$', 'about.view', name='about'),
+ url(r'^news/$', include(news_patterns, namespace='news')),
+ )
+
+
+After defining these URL patterns, Django will automatically add the
+language prefix to the URL patterns that were added by the ``i18n_patterns``
+function. Example::
+
+ from django.core.urlresolvers import reverse
+ from django.utils.translation import activate
+
+ >>> activate('en')
+ >>> reverse('sitemap_xml')
+ '/sitemap.xml'
+ >>> reverse('news:index')
+ '/en/news/'
+
+ >>> activate('nl')
+ >>> reverse('news:detail', kwargs={'slug': 'news-slug'})
+ '/nl/news/news-slug/'
+
+.. warning::
+
+ :func:`~django.conf.urls.i18n.i18n_patterns` is only allowed in your root
+ URLconf. Using it within an included URLconf will throw an
+ :exc:`ImproperlyConfigured` exception.
+
+.. warning::
+
+ Ensure that you don't have non-prefixed URL patterns that might collide
+ with an automatically-added language prefix.
+
+
+Translating URL patterns
+------------------------
+
+URL patterns can also be marked translatable using the
+:func:`~django.utils.translation.ugettext_lazy` function. Example::
+
+ from django.conf.urls.defaults import patterns, include, url
+ from django.conf.urls.i18n import i18n_patterns
+ from django.utils.translation import ugettext_lazy as _
+
+ urlpatterns = patterns(''
+ url(r'^sitemap\.xml$', 'sitemap.view', name='sitemap_xml'),
+ )
+
+ news_patterns = patterns(''
+ url(r'^$', 'news.views.index', name='index'),
+ url(_(r'^category/(?P<slug>[\w-]+)/$'), 'news.views.category', name='category'),
+ url(r'^(?P<slug>[\w-]+)/$', 'news.views.details', name='detail'),
+ )
+
+ urlpatterns += i18n_patterns('',
+ url(_(r'^about/$'), 'about.view', name='about'),
+ url(_(r'^news/$'), include(news_patterns, namespace='news')),
+ )
+
+
+After you've created the translations (see :doc:`localization` for more
+information), the :func:`~django.core.urlresolvers.reverse` function will
+return the URL in the active language. Example::
+
+ from django.core.urlresolvers import reverse
+ from django.utils.translation import activate
+
+ >>> activate('en')
+ >>> reverse('news:category', kwargs={'slug': 'recent'})
+ '/en/news/category/recent/'
+
+ >>> activate('nl')
+ >>> reverse('news:category', kwargs={'slug': 'recent'})
+ '/nl/nieuws/categorie/recent/'
+
+.. warning::
+
+ In most cases, it's best to use translated URLs only within a
+ language-code-prefixed block of patterns (using
+ :func:`~django.conf.urls.i18n.i18n_patterns`), to avoid the possibility
+ that a carelessly translated URL causes a collision with a non-translated
+ URL pattern.
+
.. _set_language-redirect-view:
The ``set_language`` redirect view
View
0  tests/regressiontests/i18n/patterns/__init__.py
No changes.
View
BIN  tests/regressiontests/i18n/patterns/locale/en/LC_MESSAGES/django.mo
Binary file not shown
View
37 tests/regressiontests/i18n/patterns/locale/en/LC_MESSAGES/django.po
@@ -0,0 +1,37 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-06-15 11:33+0200\n"
+"PO-Revision-Date: 2011-06-14 16:16+0100\n"
+"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: \n"
+
+#: urls/default.py:11
+msgid "^translated/$"
+msgstr "^translated/$"
+
+#: urls/default.py:12
+msgid "^translated/(?P<slug>[\\w-]+)/$"
+msgstr "^translated/(?P<slug>[\\w-]+)/$"
+
+#: urls/default.py:17
+msgid "^users/$"
+msgstr "^users/$"
+
+#: urls/default.py:18 urls/wrong.py:7
+msgid "^account/"
+msgstr "^account/"
+
+#: urls/namespace.py:9 urls/wrong_namespace.py:10
+msgid "^register/$"
+msgstr "^register/$"
View
BIN  tests/regressiontests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo
Binary file not shown
View
38 tests/regressiontests/i18n/patterns/locale/nl/LC_MESSAGES/django.po
@@ -0,0 +1,38 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-06-15 11:33+0200\n"
+"PO-Revision-Date: 2011-06-14 16:16+0100\n"
+"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+
+#: urls/default.py:11
+msgid "^translated/$"
+msgstr "^vertaald/$"
+
+#: urls/default.py:12
+msgid "^translated/(?P<slug>[\\w-]+)/$"
+msgstr "^vertaald/(?P<slug>[\\w-]+)/$"
+
+#: urls/default.py:17
+msgid "^users/$"
+msgstr "^gebruikers/$"
+
+#: urls/default.py:18 urls/wrong.py:7
+msgid "^account/"
+msgstr "^profiel/"
+
+#: urls/namespace.py:9 urls/wrong_namespace.py:10
+msgid "^register/$"
+msgstr "^registeren/$"
View
BIN  tests/regressiontests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.mo
Binary file not shown
View
38 tests/regressiontests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.po
@@ -0,0 +1,38 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-06-15 11:34+0200\n"
+"PO-Revision-Date: 2011-06-14 16:17+0100\n"
+"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: \n"
+"Plural-Forms: nplurals=2; plural=(n > 1)\n"
+
+#: urls/default.py:11
+msgid "^translated/$"
+msgstr "^traduzidos/$"
+
+#: urls/default.py:12
+msgid "^translated/(?P<slug>[\\w-]+)/$"
+msgstr "^traduzidos/(?P<slug>[\\w-]+)/$"
+
+#: urls/default.py:17
+msgid "^users/$"
+msgstr "^usuarios/$"
+
+#: urls/default.py:18 urls/wrong.py:7
+msgid "^account/"
+msgstr "^conta/"
+
+#: urls/namespace.py:9 urls/wrong_namespace.py:10
+msgid "^register/$"
+msgstr "^registre-se/$"
View
0  tests/regressiontests/i18n/patterns/templates/404.html
No changes.
View
0  tests/regressiontests/i18n/patterns/templates/dummy.html
No changes.
View
241 tests/regressiontests/i18n/patterns/tests.py
@@ -0,0 +1,241 @@
+import os
+
+from django.core.exceptions import ImproperlyConfigured
+from django.core.urlresolvers import reverse, clear_url_caches
+from django.test import TestCase
+from django.test.utils import override_settings
+from django.utils import translation
+
+
+class URLTestCaseBase(TestCase):
+ """
+ TestCase base-class for the URL tests.
+ """
+ urls = 'regressiontests.i18n.patterns.urls.default'
+
+ def setUp(self):
+ # Make sure the cache is empty before we are doing our tests.
+ clear_url_caches()
+
+ def tearDown(self):
+ # Make sure we will leave an empty cache for other testcases.
+ clear_url_caches()
+
+URLTestCaseBase = override_settings(
+ USE_I18N=True,
+ LOCALE_PATHS=(
+ os.path.join(os.path.dirname(__file__), 'locale'),
+ ),
+ TEMPLATE_DIRS=(
+ os.path.join(os.path.dirname(__file__), 'templates'),
+ ),
+ LANGUAGE_CODE='en',
+ LANGUAGES=(
+ ('nl', 'Dutch'),
+ ('en', 'English'),
+ ('pt-br', 'Brazilian Portuguese'),
+ ),
+ MIDDLEWARE_CLASSES=(
+ 'django.middleware.locale.LocaleMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ ),
+)(URLTestCaseBase)
+
+
+class URLPrefixTests(URLTestCaseBase):
+ """
+ Tests if the `i18n_patterns` is adding the prefix correctly.
+ """
+ def test_not_prefixed(self):
+ with translation.override('en'):
+ self.assertEqual(reverse('not-prefixed'), '/not-prefixed/')
+ with translation.override('nl'):
+ self.assertEqual(reverse('not-prefixed'), '/not-prefixed/')
+
+ def test_prefixed(self):
+ with translation.override('en'):
+ self.assertEqual(reverse('prefixed'), '/en/prefixed/')
+ with translation.override('nl'):
+ self.assertEqual(reverse('prefixed'), '/nl/prefixed/')
+
+ @override_settings(ROOT_URLCONF='regressiontests.i18n.patterns.urls.wrong')
+ def test_invalid_prefix_use(self):
+ self.assertRaises(ImproperlyConfigured, lambda: reverse('account:register'))
+
+
+class URLDisabledTests(URLTestCaseBase):
+ urls = 'regressiontests.i18n.patterns.urls.disabled'
+
+ @override_settings(USE_I18N=False)
+ def test_prefixed_i18n_disabled(self):
+ with translation.override('en'):
+ self.assertEqual(reverse('prefixed'), '/prefixed/')
+ with translation.override('nl'):
+ self.assertEqual(reverse('prefixed'), '/prefixed/')
+
+
+class URLTranslationTests(URLTestCaseBase):
+ """
+ Tests if the pattern-strings are translated correctly (within the
+ `i18n_patterns` and the normal `patterns` function).
+ """
+ def test_no_prefix_translated(self):
+ with translation.override('en'):
+ self.assertEqual(reverse('no-prefix-translated'), '/translated/')
+ self.assertEqual(reverse('no-prefix-translated-slug', kwargs={'slug': 'yeah'}), '/translated/yeah/')
+
+ with translation.override('nl'):
+ self.assertEqual(reverse('no-prefix-translated'), '/vertaald/')
+ self.assertEqual(reverse('no-prefix-translated-slug', kwargs={'slug': 'yeah'}), '/vertaald/yeah/')
+
+ with translation.override('pt-br'):
+ self.assertEqual(reverse('no-prefix-translated'), '/traduzidos/')
+ self.assertEqual(reverse('no-prefix-translated-slug', kwargs={'slug': 'yeah'}), '/traduzidos/yeah/')
+
+ def test_users_url(self):
+ with translation.override('en'):
+ self.assertEqual(reverse('users'), '/en/users/')
+
+ with translation.override('nl'):
+ self.assertEqual(reverse('users'), '/nl/gebruikers/')
+
+ with translation.override('pt-br'):
+ self.assertEqual(reverse('users'), '/pt-br/usuarios/')
+
+
+class URLNamespaceTests(URLTestCaseBase):
+ """
+ Tests if the translations are still working within namespaces.
+ """
+ def test_account_register(self):
+ with translation.override('en'):
+ self.assertEqual(reverse('account:register'), '/en/account/register/')
+
+ with translation.override('nl'):
+ self.assertEqual(reverse('account:register'), '/nl/profiel/registeren/')
+
+
+class URLRedirectTests(URLTestCaseBase):
+ """
+ Tests if the user gets redirected to the right URL when there is no
+ language-prefix in the request URL.
+ """
+ def test_no_prefix_response(self):
+ response = self.client.get('/not-prefixed/')
+ self.assertEqual(response.status_code, 200)
+
+ def test_en_redirect(self):
+ response = self.client.get('/account/register/', HTTP_ACCEPT_LANGUAGE='en')
+ self.assertRedirects(response, 'http://testserver/en/account/register/')
+
+ response = self.client.get(response['location'])
+ self.assertEqual(response.status_code, 200)
+
+ def test_en_redirect_wrong_url(self):
+ response = self.client.get('/profiel/registeren/', HTTP_ACCEPT_LANGUAGE='en')
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response['location'], 'http://testserver/en/profiel/registeren/')
+
+ response = self.client.get(response['location'])
+ self.assertEqual(response.status_code, 404)
+
+ def test_nl_redirect(self):
+ response = self.client.get('/profiel/registeren/', HTTP_ACCEPT_LANGUAGE='nl')
+ self.assertRedirects(response, 'http://testserver/nl/profiel/registeren/')
+
+ response = self.client.get(response['location'])
+ self.assertEqual(response.status_code, 200)
+
+ def test_nl_redirect_wrong_url(self):
+ response = self.client.get('/account/register/', HTTP_ACCEPT_LANGUAGE='nl')
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response['location'], 'http://testserver/nl/account/register/')
+
+ response = self.client.get(response['location'])
+ self.assertEqual(response.status_code, 404)
+
+ def test_pt_br_redirect(self):
+ response = self.client.get('/conta/registre-se/', HTTP_ACCEPT_LANGUAGE='pt-br')
+ self.assertRedirects(response, 'http://testserver/pt-br/conta/registre-se/')
+
+ response = self.client.get(response['location'])
+ self.assertEqual(response.status_code, 200)
+
+
+class URLRedirectWithoutTrailingSlashTests(URLTestCaseBase):
+ """
+ Tests the redirect when the requested URL doesn't end with a slash
+ (`settings.APPEND_SLASH=True`).
+ """
+ def test_not_prefixed_redirect(self):
+ response = self.client.get('/not-prefixed', HTTP_ACCEPT_LANGUAGE='en')
+ self.assertEqual(response.status_code, 301)
+ self.assertEqual(response['location'], 'http://testserver/not-prefixed/')
+
+ def test_en_redirect(self):
+ response = self.client.get('/account/register', HTTP_ACCEPT_LANGUAGE='en')
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response['location'], 'http://testserver/en/account/register')
+
+ response = self.client.get(response['location'])
+ self.assertEqual(response.status_code, 301)
+ self.assertEqual(response['location'], 'http://testserver/en/account/register/')
+
+
+class URLRedirectWithoutTrailingSlashSettingTests(URLTestCaseBase):
+ """
+ Tests the redirect when the requested URL doesn't end with a slash
+ (`settings.APPEND_SLASH=False`).
+ """
+ @override_settings(APPEND_SLASH=False)
+ def test_not_prefixed_redirect(self):
+ response = self.client.get('/not-prefixed', HTTP_ACCEPT_LANGUAGE='en')
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response['location'], 'http://testserver/en/not-prefixed')
+
+ response = self.client.get(response['location'])
+ self.assertEqual(response.status_code, 404)
+
+ @override_settings(APPEND_SLASH=False)
+ def test_en_redirect(self):
+ response = self.client.get('/account/register', HTTP_ACCEPT_LANGUAGE='en')
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response['location'], 'http://testserver/en/account/register')
+
+ response = self.client.get(response['location'])
+ self.assertEqual(response.status_code, 404)
+
+
+class URLResponseTests(URLTestCaseBase):
+ """
+ Tests if the response has the right language-code.
+ """
+ def test_not_prefixed_with_prefix(self):
+ response = self.client.get('/en/not-prefixed/')
+ self.assertEqual(response.status_code, 404)
+
+ def test_en_url(self):
+ response = self.client.get('/en/account/register/')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response['content-language'], 'en')
+ self.assertEqual(response.context['LANGUAGE_CODE'], 'en')
+
+ def test_nl_url(self):
+ response = self.client.get('/nl/profiel/registeren/')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response['content-language'], 'nl')
+ self.assertEqual(response.context['LANGUAGE_CODE'], 'nl')
+
+ def test_wrong_en_prefix(self):
+ response = self.client.get('/en/profiel/registeren/')
+ self.assertEqual(response.status_code, 404)
+
+ def test_wrong_nl_prefix(self):
+ response = self.client.get('/nl/account/register/')
+ self.assertEqual(response.status_code, 404)
+
+ def test_pt_br_url(self):
+ response = self.client.get('/pt-br/conta/registre-se/')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response['content-language'], 'pt-br')
+ self.assertEqual(response.context['LANGUAGE_CODE'], 'pt-br')
View
0  tests/regressiontests/i18n/patterns/urls/__init__.py
No changes.
View
19 tests/regressiontests/i18n/patterns/urls/default.py
@@ -0,0 +1,19 @@
+from django.conf.urls.defaults import patterns, include, url
+from django.conf.urls.i18n import i18n_patterns
+from django.utils.translation import ugettext_lazy as _
+from django.views.generic import TemplateView
+
+
+view = TemplateView.as_view(template_name='dummy.html')
+
+urlpatterns = patterns('',
+ url(r'^not-prefixed/$', view, name='not-prefixed'),
+ url(_(r'^translated/$'), view, name='no-prefix-translated'),
+ url(_(r'^translated/(?P<slug>[\w-]+)/$'), view, name='no-prefix-translated-slug'),
+)
+
+urlpatterns += i18n_patterns('',
+ url(r'^prefixed/$', view, name='prefixed'),
+ url(_(r'^users/$'), view, name='users'),
+ url(_(r'^account/'), include('regressiontests.i18n.patterns.urls.namespace', namespace='account')),
+)
View
9 tests/regressiontests/i18n/patterns/urls/disabled.py
@@ -0,0 +1,9 @@
+from django.conf.urls.defaults import url
+from django.conf.urls.i18n import i18n_patterns
+from django.views.generic import TemplateView
+
+view = TemplateView.as_view(template_name='dummy.html')
+
+urlpatterns = i18n_patterns('',
+ url(r'^prefixed/$', view, name='prefixed'),
+)
View
10 tests/regressiontests/i18n/patterns/urls/namespace.py
@@ -0,0 +1,10 @@
+from django.conf.urls.defaults import patterns, include, url
+from django.utils.translation import ugettext_lazy as _
+from django.views.generic import TemplateView
+
+
+view = TemplateView.as_view(template_name='dummy.html')
+
+urlpatterns = patterns('',
+ url(_(r'^register/$'), view, name='register'),
+)
View
8 tests/regressiontests/i18n/patterns/urls/wrong.py
@@ -0,0 +1,8 @@
+from django.conf.urls.defaults import patterns, include, url
+from django.conf.urls.i18n import i18n_patterns
+from django.utils.translation import ugettext_lazy as _
+
+
+urlpatterns = i18n_patterns('',
+ url(_(r'^account/'), include('regressiontests.i18n.patterns.urls.wrong_namespace', namespace='account')),
+)
View
11 tests/regressiontests/i18n/patterns/urls/wrong_namespace.py
@@ -0,0 +1,11 @@
+from django.conf.urls.defaults import include, url
+from django.conf.urls.i18n import i18n_patterns
+from django.utils.translation import ugettext_lazy as _
+from django.views.generic import TemplateView
+
+
+view = TemplateView.as_view(template_name='dummy.html')
+
+urlpatterns = i18n_patterns('',
+ url(_(r'^register/$'), view, name='register'),
+)
View
19 tests/regressiontests/i18n/tests.py
@@ -3,13 +3,12 @@
import datetime
import decimal
import os
-import sys
import pickle
from threading import local
from django.conf import settings
from django.template import Template, Context
-from django.test import TestCase
+from django.test import TestCase, RequestFactory
from django.utils.formats import (get_format, date_format, time_format,
localize, localize_input, iter_format_modules, get_format_modules)
from django.utils.importlib import import_module
@@ -18,14 +17,14 @@
from django.utils import translation
from django.utils.translation import (ugettext, ugettext_lazy, activate,
deactivate, gettext_lazy, pgettext, npgettext, to_locale,
- get_language_info, get_language)
+ get_language_info, get_language, get_language_from_request)
from forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm
from models import Company, TestModel
from commands.tests import *
-
+from patterns.tests import *
from test_warnings import DeprecationWarningTests
class TranslationTests(TestCase):
@@ -494,6 +493,9 @@ def test_localize_templatetag_and_filter(self):
class MiscTests(TestCase):
+ def setUp(self):
+ self.rf = RequestFactory()
+
def test_parse_spec_http_header(self):
"""
Testing HTTP header parsing. First, we test that we can parse the
@@ -534,10 +536,8 @@ def test_parse_literal_http_header(self):
"""
Now test that we parse a literal HTTP header correctly.
"""
- from django.utils.translation.trans_real import get_language_from_request
g = get_language_from_request
- from django.http import HttpRequest
- r = HttpRequest
+ r = self.rf.get('/')
r.COOKIES = {}
r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt-br'}
self.assertEqual('pt-br', g(r))
@@ -569,10 +569,8 @@ def test_parse_language_cookie(self):
"""
Now test that we parse language preferences stored in a cookie correctly.
"""
- from django.utils.translation.trans_real import get_language_from_request
g = get_language_from_request
- from django.http import HttpRequest
- r = HttpRequest
+ r = self.rf.get('/')
r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'pt-br'}
r.META = {}
self.assertEqual('pt-br', g(r))
@@ -827,4 +825,3 @@ def test_multiple_locale_direct_switch_btrans(self):
t = Template("{% load i18n %}{% blocktrans %}No{% endblocktrans %}")
with translation.override('nl'):
self.assertEqual(t.render(Context({})), 'Nee')
-
Please sign in to comment.
Something went wrong with that request. Please try again.