├── tests
├── __init__.py
├── garfield
│ ├── __init__.py
│ ├── locale
│ │ └── fr
│ │ │ └── LC_MESSAGES
│ │ │ ├── django.mo
│ │ │ └── django.po
│ ├── extra_urls.py
│ ├── admin.py
│ ├── urls.py
│ ├── fixtures
│ │ └── test.json
│ ├── models.py
│ ├── views.py
│ └── tests.py
├── templates
│ ├── 404.html
│ ├── home.html
│ ├── multilang_home.html
│ ├── garfield
│ │ ├── the_president.html
│ │ ├── landing.html
│ │ ├── comicstrip_detail.html
│ │ └── comicstrip_list.html
│ ├── about_us.html
│ └── base.html
├── run_tests.py
├── urls_without_lang_prefix.py
├── urls.py
└── settings.py
├── transurlvania
├── __init__.py
├── templatetags
│ ├── __init__.py
│ └── transurlvania_tags.py
├── locale
│ ├── en
│ │ └── LC_MESSAGES
│ │ │ ├── django.mo
│ │ │ └── django.po
│ └── fr
│ │ └── LC_MESSAGES
│ │ ├── django.mo
│ │ └── django.po
├── context_processors.py
├── settings.py
├── views.py
├── choices.py
├── templates
│ └── admin
│ │ ├── ml_change_form.html
│ │ └── includes
│ │ └── ml_links.html
├── utils.py
├── decorators.py
├── defaults.py
├── middleware.py
├── translators.py
└── urlresolvers.py
├── .gitignore
├── AUTHORS
├── MANIFEST.in
├── ROADMAP.rst
├── setup.py
├── RELEASES.rst
├── LICENSE
└── README.rst
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/garfield/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/transurlvania/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/templates/404.html:
--------------------------------------------------------------------------------
1 | Page not found
--------------------------------------------------------------------------------
/transurlvania/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/templates/home.html:
--------------------------------------------------------------------------------
1 |
Welcome home
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .*project
2 | dist/
3 | *.egg-info/
4 |
5 |
--------------------------------------------------------------------------------
/tests/templates/multilang_home.html:
--------------------------------------------------------------------------------
1 | This is the language-independent home template.
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | Primary Authors:
2 |
3 | * Sam Bull
4 | * Zach Mathew
5 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include AUTHORS
2 | include LICENSE
3 | include README.rst
4 | include RELEASES.rst
5 | include ROADMAP.rst
--------------------------------------------------------------------------------
/transurlvania/locale/en/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trapeze/transurlvania/HEAD/transurlvania/locale/en/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/transurlvania/locale/fr/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trapeze/transurlvania/HEAD/transurlvania/locale/fr/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/tests/garfield/locale/fr/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trapeze/transurlvania/HEAD/tests/garfield/locale/fr/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/tests/templates/garfield/the_president.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% trans "Blow up the earth? I hope not! It's where I keep all my stuff!" %}
--------------------------------------------------------------------------------
/tests/templates/about_us.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block content %}
4 | We're just some people who want to disambiguate the name Garfield
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/tests/templates/garfield/landing.html:
--------------------------------------------------------------------------------
1 | {% load transurlvania_tags %}
2 |
3 | Things and provisions.
4 |
5 | French Version
--------------------------------------------------------------------------------
/transurlvania/context_processors.py:
--------------------------------------------------------------------------------
1 | def translate(request):
2 | url_translator = getattr(request, 'url_translator', None)
3 | if url_translator:
4 | return {'_url_translator': url_translator}
5 | else:
6 | return {}
--------------------------------------------------------------------------------
/transurlvania/settings.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 |
4 | HIDE_TRANSLATABLE_APPS = getattr(settings, "MULTILANG_HIDE_TRANSLATABLE_APPS", False)
5 |
6 |
7 | LANGUAGE_DOMAINS = getattr(settings, "MULTILANG_LANGUAGE_DOMAINS", {})
8 |
--------------------------------------------------------------------------------
/tests/garfield/extra_urls.py:
--------------------------------------------------------------------------------
1 | from django.utils.translation import ugettext_noop as _
2 |
3 | from transurlvania.defaults import *
4 |
5 |
6 | urlpatterns = patterns('garfield.views',
7 | url(r'^jim-davis/$', 'jim_davis', name='garfield_jim_davis'),
8 | )
9 |
--------------------------------------------------------------------------------
/tests/templates/garfield/comicstrip_detail.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block content %}
6 |
7 | {% trans "Garfield Comic Strip: " %}{{ object.headline }}
8 |
9 | {{ object.comic }}
10 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/transurlvania/views.py:
--------------------------------------------------------------------------------
1 | from django.http import HttpResponseRedirect
2 | from django.utils.translation import get_language_from_request
3 |
4 |
5 | def detect_language_and_redirect(request):
6 | return HttpResponseRedirect(
7 | '/%s/' % get_language_from_request(request)
8 | )
9 |
--------------------------------------------------------------------------------
/tests/garfield/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from garfield.models import ComicStrip
4 |
5 |
6 | class ComicStripAdmin(admin.ModelAdmin):
7 | list_display = ('name', 'language', 'publication_date')
8 | list_filter = ('language',)
9 |
10 |
11 | admin.site.register(ComicStrip, ComicStripAdmin)
12 |
--------------------------------------------------------------------------------
/transurlvania/choices.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.utils.translation import ugettext_lazy as _
3 |
4 |
5 | # We need to wrap the language description in the real ugettext if we
6 | # want the translation of the language names to be available (in the admin for
7 | # instance).
8 |
9 | LANGUAGES_CHOICES = [
10 | (code, _(description)) for (code, description) in settings.LANGUAGES
11 | ]
12 |
--------------------------------------------------------------------------------
/tests/templates/garfield/comicstrip_list.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 | {% block title %}{{ block.super }} - News Stories{% endblock %}
4 |
5 |
6 | {% trans "News Stories" %}
7 |
8 |
9 | {% for news_story in object_list %}
10 | -
11 |
12 | {{ news_story.body }}
13 |
14 | {% endfor %}
15 |
16 |
--------------------------------------------------------------------------------
/tests/run_tests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os, os.path
4 | import sys
5 | import pprint
6 |
7 | path, scriptname = os.path.split(__file__)
8 |
9 | sys.path.append(os.path.abspath(path))
10 | sys.path.append(os.path.abspath(os.path.join(path, '..')))
11 |
12 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings'
13 |
14 | from django.core import management
15 |
16 | management.call_command('test', 'garfield')
17 |
--------------------------------------------------------------------------------
/tests/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load transurlvania_tags %}
2 |
4 |
5 |
6 |
7 |
8 | {% block content %}{% endblock %}
9 | French Version
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tests/urls_without_lang_prefix.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.utils.translation import ugettext_noop as _
3 |
4 | from transurlvania.defaults import *
5 |
6 | admin.autodiscover()
7 |
8 | urlpatterns = patterns('garfield.views',
9 | url(r'^$', 'home'),
10 | url(r'^admin/', include(admin.site.urls)),
11 | url(r'^garfield/', include('garfield.urls')),
12 | url(_(r'^about-us/$'), 'about_us', name='about_us'),
13 | )
14 |
--------------------------------------------------------------------------------
/transurlvania/templates/admin/ml_change_form.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/change_form.html" %}
2 |
3 | {% block bodyclass %}{{ block.super }}{% if not trans_core %} trans-hide-core{% if trans_hide_lang %} trans-hide-lang{% endif %}{% endif %}{% endblock %}
4 |
5 | {% block breadcrumbs %}{% if not trans_hide_breadcrumbs %}{{ block.super }}{% endif %}{% endblock %}
6 |
7 | {% block form_top %}
8 | {% include "admin/includes/ml_links.html" %}
9 | {% endblock %}
10 |
11 | {% block after_related_objects %}
12 | {% include "admin/includes/ml_links.html" %}
13 | {% endblock %}
--------------------------------------------------------------------------------
/tests/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.utils.translation import ugettext_noop as _
3 |
4 | from transurlvania.defaults import *
5 |
6 | admin.autodiscover()
7 |
8 | urlpatterns = lang_prefixed_patterns('garfield.views',
9 | url(r'^$', 'home'),
10 | url(r'^admin/', include(admin.site.urls)),
11 | (r'^garfield/', include('garfield.urls')),
12 | url(_(r'^about-us/$'), 'about_us', name='about_us'),
13 | )
14 |
15 |
16 | urlpatterns += patterns('transurlvania.views',
17 | (r'^$', 'detect_language_and_redirect'),
18 | )
19 |
--------------------------------------------------------------------------------
/tests/garfield/urls.py:
--------------------------------------------------------------------------------
1 | from django.utils.translation import ugettext_noop as _
2 |
3 | from transurlvania.defaults import *
4 |
5 |
6 | urlpatterns = patterns('garfield.views',
7 | url(r'^$', 'landing', name='garfield_landing'),
8 | url(_(r'^the-president/$'), 'the_president', name='garfield_the_president'),
9 | (_(r'^the-cat/$'), 'comic_strip_list', {}, 'garfield_the_cat'),
10 | url(_(r'^the-cat/(?:P\d+)/$'), 'comic_strip_detail',
11 | name='garfield_comic_strip_detail'),
12 | url(r'', include('garfield.extra_urls')),
13 | )
14 |
--------------------------------------------------------------------------------
/ROADMAP.rst:
--------------------------------------------------------------------------------
1 | Multilang URLs Roadmap
2 | ======================
3 |
4 | Future
5 | ------
6 |
7 | * Sticky language selection
8 |
9 | * When a user requests a lang-prefixed page, the middleware sets their
10 | language cookie so that they'll be sent back to that same language if
11 | they arrive back at the language-agnostic root URL
12 |
13 | * Refactor MultilangRegexURLResolver so that it properly handles app_name
14 | and name_space
15 |
16 | * Build separate gettext wrapper for translatable URLs
17 |
18 | * Remove requirement that translatable URL patterns be wrapped in gettext
19 | blocks
20 |
21 | * Remove requirement that translatable URLs be wrapped in explicit "url"
22 | function
--------------------------------------------------------------------------------
/transurlvania/utils.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ImproperlyConfigured
2 | from django.utils.translation import get_language
3 |
4 | from transurlvania.settings import LANGUAGE_DOMAINS
5 |
6 |
7 | def complete_url(url, lang=None):
8 | """
9 | Takes a url (or path) and returns a full url including the appropriate
10 | domain name (based on the LANGUAGE_DOMAINS setting).
11 | """
12 | if not url.startswith('http://'):
13 | lang = lang or get_language()
14 | domain = LANGUAGE_DOMAINS.get(lang)
15 | if domain:
16 | url = u'http://%s%s' % (domain[0], url)
17 | else:
18 | raise ImproperlyConfigured(
19 | 'Not domain specified for language code %s' % lang
20 | )
21 | return url
22 |
--------------------------------------------------------------------------------
/transurlvania/templates/admin/includes/ml_links.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
20 |
--------------------------------------------------------------------------------
/tests/garfield/fixtures/test.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "pk": 1,
4 | "model": "garfield.comicstrip",
5 | "fields": {
6 | "name": "English Comic Strip #1",
7 | "comic": "no-file",
8 | "language": "en",
9 | "publication_date": "2009-01-26 16:05:05",
10 | "public": true
11 | }
12 | },
13 | {
14 | "pk": 2,
15 | "model": "garfield.comicstrip",
16 | "fields": {
17 | "name": "Comic Strip Français #1",
18 | "comic": "no-file",
19 | "language": "fr",
20 | "publication_date": "2009-01-26 16:05:05",
21 | "public": true
22 | }
23 | },
24 | {
25 | "pk": 3,
26 | "model": "garfield.comicstrip",
27 | "fields": {
28 | "name": "English Comic Strip #2",
29 | "comic": "no-file",
30 | "language": "en",
31 | "publication_date": "2009-01-26 16:05:05",
32 | "public": true
33 | }
34 | }
35 | ]
36 |
--------------------------------------------------------------------------------
/tests/garfield/models.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from django.conf import settings
4 | from django.db import models
5 | from django.utils.translation import ugettext_lazy as _
6 |
7 | from transurlvania.decorators import permalink_in_lang
8 |
9 |
10 | class ComicStrip(models.Model):
11 | """
12 | A Garfield comic strip
13 | """
14 | name = models.CharField(_('name'), max_length=255)
15 | comic = models.URLField(_('comic'))
16 | publication_date = models.DateTimeField(_('publish date/time'),
17 | default=datetime.datetime.now)
18 | public = models.BooleanField(_('public'), default=True)
19 | language = models.CharField(_('language'), max_length=5,
20 | choices=settings.LANGUAGES)
21 |
22 | class Meta:
23 | verbose_name = _('comic strip')
24 | verbose_name_plural = _('comic strips')
25 |
26 | def __unicode__(self):
27 | return self.name
28 |
29 | @permalink_in_lang
30 | def get_absolute_url(self):
31 | return ('garfield_comic_strip_detail', self.language, (), {'slug': self.slug,})
32 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | from setuptools import setup, find_packages
5 |
6 | f = open(os.path.join(os.path.dirname(__file__), 'README.rst'))
7 | readme = f.read()
8 | f.close()
9 |
10 | setup(
11 | name='transurlvania',
12 | version='0.2.4',
13 | author='Sam Bull',
14 | author_email='sam@pocketuniverse.ca',
15 | url='https://github.com/trapeze/transurlvania',
16 | description="This application provides a collection of URL-related utilities for multi-lingual projects.",
17 | long_description=readme,
18 | packages=find_packages(exclude=['tests', 'tests.garfield']),
19 | classifiers=[
20 | 'Framework :: Django',
21 | #'Development Status :: 5 - Production/Stable',
22 | 'Environment :: Web Environment',
23 | 'Programming Language :: Python',
24 | 'Intended Audience :: Developers',
25 | 'Operating System :: OS Independent',
26 | 'License :: OSI Approved :: BSD License',
27 | 'Topic :: Software Development :: Libraries :: Python Modules',
28 | ],
29 | install_requires=[
30 | 'Django>=1.0',
31 | ],
32 | include_package_data=True,
33 | )
34 |
--------------------------------------------------------------------------------
/transurlvania/decorators.py:
--------------------------------------------------------------------------------
1 | from transurlvania.translators import BasicScheme, ObjectBasedScheme, DirectToURLScheme
2 |
3 |
4 | def translate_using_url(url_name):
5 | return _translate_using(DirectToURLScheme(url_name))
6 |
7 |
8 | def translate_using_object(object_name):
9 | return _translate_using(ObjectBasedScheme(object_name))
10 |
11 |
12 | def translate_using_custom_scheme(scheme):
13 | return _translate_using(scheme)
14 |
15 |
16 | def do_not_translate(view_func):
17 | return _translate_using(BasicScheme())(view_func)
18 |
19 |
20 | def _translate_using(scheme):
21 | def translate_decorator(view_func):
22 | def inner(request, *args, **kwargs):
23 | if hasattr(request, 'url_translator'):
24 | request.url_translator.scheme = scheme
25 | return view_func(request, *args, **kwargs)
26 | return inner
27 | return translate_decorator
28 |
29 |
30 | def permalink_in_lang(func):
31 | from transurlvania.urlresolvers import reverse_for_language
32 | def inner(*args, **kwargs):
33 | bits = func(*args, **kwargs)
34 | return reverse_for_language(bits[0], bits[1], None, *bits[2:4])
35 | return inner
36 |
--------------------------------------------------------------------------------
/tests/garfield/views.py:
--------------------------------------------------------------------------------
1 | from django.views.generic import simple, list_detail
2 | from django.http import HttpResponse
3 |
4 | from garfield.models import ComicStrip
5 |
6 |
7 | def home(request):
8 | return simple.direct_to_template(request, 'home.html')
9 |
10 |
11 | def about_us(request):
12 | return simple.direct_to_template(request, 'about_us.html')
13 |
14 |
15 | def landing(request):
16 | return simple.direct_to_template(request, 'garfield/landing.html')
17 |
18 |
19 | def the_president(request):
20 | return simple.direct_to_template(request, 'garfield/the_president.html')
21 |
22 |
23 | def comic_strip_list(request):
24 | return list_detail.object_list(
25 | request,
26 | queryset=ComicStrip.objects.filter(language=request.LANGUAGE_CODE),
27 | template_object_name='comic_strip',
28 | )
29 |
30 |
31 | def comic_strip_detail(request, strip_id):
32 | return list_detail.object_detail(
33 | request,
34 | ComicStrip.objects.filter(language=request.LANGUAGE_CODE),
35 | id=strip_id,
36 | template_object_name='comic_strip',
37 | )
38 |
39 |
40 | def jim_davis(request):
41 | return HttpResponse('Jim Davis is the creator of Garfield')
42 |
--------------------------------------------------------------------------------
/RELEASES.rst:
--------------------------------------------------------------------------------
1 | 0.2.4
2 | =====
3 |
4 | Tagged on November 4, 2011 by Sam Bull
5 |
6 | * Turned error strings into unicode strings
7 |
8 | 0.2.3
9 | =====
10 |
11 | Tagged on March 12, 2011 by Sam Bull
12 |
13 | * Fixed empty regex issue
14 | * Added support for URL patterns without explicit call to url function
15 |
16 | 0.2.2
17 | =====
18 |
19 | Tagged on January 5, 2011 by Sam Bull
20 |
21 | * Fixed test code to remove PIL dependency.
22 |
23 | 0.2.1
24 | =====
25 |
26 | Tagged on January 5, 2011 by Sam Bull
27 |
28 | * Added setup.py
29 | * Made test runner compatible with virtualenv
30 | * Cleaned up roadmap
31 | * Fixed typo in readme
32 |
33 | transurlvania-0.2.0ALPHA
34 | ========================
35 |
36 | Tagged on June 20, 2010 by Sam Bull (sbull@trapeze.com)
37 |
38 | * Renamed app to transurlvania to enhance distinctiveness / awesomeness
39 | * Renamed template tags to transurlvania_tags
40 | * Renamed the MultilangMiddleware middleware to URLTransMiddleware
41 | * Refactored language-in-path component to make it easier to use
42 | * Simplified the API for making URLs localizable
43 | * Updated documentation
44 | * Refactored test code to make the example code less abstract
45 |
46 | ----
47 |
48 | multilang_urls-0.1.0ALPHA
49 | =========================
50 |
51 | Tagged on April 25, 2010 by Sam Bull (sbull@trapeze.com)
52 |
53 | Initial release from existing codebase.
54 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010, Trapeze
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright
10 | notice, this list of conditions and the following disclaimer in the
11 | documentation and/or other materials provided with the distribution.
12 | * Neither the name of Trapeze Media nor the names of its contributors
13 | may be used to endorse or promote products derived from this software
14 | without specific prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 | POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/transurlvania/defaults.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import *
2 | from django.core.urlresolvers import RegexURLPattern
3 |
4 | from transurlvania.urlresolvers import LangSelectionRegexURLResolver
5 | from transurlvania.urlresolvers import MultilangRegexURLResolver
6 | from transurlvania.urlresolvers import MultilangRegexURLPattern
7 | from transurlvania.urlresolvers import PocketURLModule
8 |
9 |
10 | def lang_prefixed_patterns(prefix, *args):
11 | pattern_list = patterns(prefix, *args)
12 | return [LangSelectionRegexURLResolver(PocketURLModule(pattern_list))]
13 |
14 |
15 | def url(regex, view, kwargs=None, name=None, prefix=''):
16 | # Copied from django.conf.urls.defaults.url
17 | if isinstance(view, (list,tuple)):
18 | # For include(...) processing.
19 | urlconf_module, app_name, namespace = view
20 | return MultilangRegexURLResolver(regex, urlconf_module, kwargs, app_name=app_name, namespace=namespace)
21 | else:
22 | if isinstance(view, basestring):
23 | if not view:
24 | raise ImproperlyConfigured('Empty URL pattern view name not permitted (for pattern %r)' % regex)
25 | if prefix:
26 | view = prefix + '.' + view
27 | return MultilangRegexURLPattern(regex, view, kwargs, name)
28 |
29 | # Copied from django.conf.urls.defaults so that it's invoking the url func
30 | # defined here instead of the built-in one.
31 | def patterns(prefix, *args):
32 | pattern_list = []
33 | for t in args:
34 | if isinstance(t, (list, tuple)):
35 | t = url(prefix=prefix, *t)
36 | elif isinstance(t, RegexURLPattern):
37 | t.add_prefix(prefix)
38 | pattern_list.append(t)
39 | return pattern_list
40 |
--------------------------------------------------------------------------------
/tests/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | DATABASE_ENGINE = 'sqlite3'
4 | DATABASE_NAME = os.path.join(os.path.dirname(__file__), 'transurlvania_test.db')
5 |
6 | LANGUAGE_CODE = 'en'
7 |
8 | gettext = lambda s: s
9 |
10 | LANGUAGES = (
11 | ('en', gettext('English')),
12 | ('fr', gettext('French')),
13 | ('de', gettext('German')),
14 | )
15 |
16 | MULTILANG_LANGUAGE_DOMAINS = {
17 | 'en': ('www.trapeze-en.com', 'English Site'),
18 | 'fr': ('www.trapeze-fr.com', 'French Site')
19 | }
20 |
21 | MIDDLEWARE_CLASSES = (
22 | 'transurlvania.middleware.URLCacheResetMiddleware',
23 | 'django.contrib.sessions.middleware.SessionMiddleware',
24 | 'django.middleware.locale.LocaleMiddleware',
25 | 'transurlvania.middleware.LangInPathMiddleware',
26 | 'transurlvania.middleware.LangInDomainMiddleware',
27 | 'django.middleware.common.CommonMiddleware',
28 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
29 | 'django.middleware.doc.XViewMiddleware',
30 | 'transurlvania.middleware.URLTransMiddleware',
31 | )
32 |
33 | ROOT_URLCONF = 'tests.urls'
34 |
35 | TEMPLATE_CONTEXT_PROCESSORS = (
36 | 'django.core.context_processors.auth',
37 | 'django.core.context_processors.debug',
38 | 'django.core.context_processors.i18n',
39 | 'django.core.context_processors.media',
40 | 'django.core.context_processors.request',
41 | 'transurlvania.context_processors.translate',
42 | )
43 |
44 | INSTALLED_APPS = (
45 | 'django.contrib.auth',
46 | 'django.contrib.admin',
47 | 'django.contrib.contenttypes',
48 | 'django.contrib.sessions',
49 |
50 | 'transurlvania',
51 | 'garfield',
52 | )
53 |
54 | TEMPLATE_DIRS = (
55 | os.path.join(os.path.realpath(os.path.dirname(__file__)), 'templates/'),
56 | )
57 |
--------------------------------------------------------------------------------
/tests/garfield/locale/fr/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
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 , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2010-05-04 18:18-0400\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: FULL NAME \n"
14 | "Language-Team: LANGUAGE \n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 |
19 | #: lang_prefixed_urls.py:12 urls.py:12
20 | msgid "^about-us/$"
21 | msgstr "^a-propos-de-nous/$"
22 |
23 | #: settings.py:11
24 | msgid "English"
25 | msgstr ""
26 |
27 | #: settings.py:12
28 | msgid "French"
29 | msgstr ""
30 |
31 | #: settings.py:13
32 | msgid "German"
33 | msgstr ""
34 |
35 | #: garfield/models.py:14
36 | msgid "name"
37 | msgstr ""
38 |
39 | #: garfield/models.py:15
40 | msgid "comic"
41 | msgstr ""
42 |
43 | #: garfield/models.py:16
44 | msgid "publish date/time"
45 | msgstr ""
46 |
47 | #: garfield/models.py:18
48 | msgid "public"
49 | msgstr ""
50 |
51 | #: garfield/models.py:19
52 | msgid "language"
53 | msgstr ""
54 |
55 | #: garfield/models.py:23
56 | msgid "comic strip"
57 | msgstr ""
58 |
59 | #: garfield/models.py:24
60 | msgid "comic strips"
61 | msgstr ""
62 |
63 | #: garfield/urls.py:8
64 | msgid "^the-president/$"
65 | msgstr "^le-président/$"
66 |
67 | #: garfield/urls.py:9
68 | msgid "^the-cat/$"
69 | msgstr "^le-chat/$"
70 |
71 | #: garfield/urls.py:10
72 | msgid "^the-cat/(?:P\\d+)/$"
73 | msgstr "^le-chat/(?:P\\d+)/$"
74 |
75 | #: templates/garfield/comic_strip_detail.html:8
76 | msgid "Garfield Comic Strip: "
77 | msgstr ""
78 |
79 | #: templates/garfield/comic_strip_list.html:6
80 | msgid "News Stories"
81 | msgstr ""
82 |
83 | #: templates/garfield/the_president.html:2
84 | msgid "Blow up the earth? I hope not! It's where I keep all my stuff!"
85 | msgstr ""
86 |
--------------------------------------------------------------------------------
/transurlvania/locale/en/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
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 , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2009-01-26 17:59-0500\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: FULL NAME \n"
14 | "Language-Team: LANGUAGE \n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 |
19 | #: admin.py:109
20 | #, python-format
21 | msgid "Field %(field)s could not be found in the model %(model)s."
22 | msgstr ""
23 |
24 | #: admin.py:114
25 | #, python-format
26 | msgid "Field %(field)s in the model %(model)s is not a ForeignKey."
27 | msgstr ""
28 |
29 | #: admin.py:122 admin.py:224
30 | #, python-format
31 | msgid "The %(name)s \"%(obj)s\" was added successfully."
32 | msgstr ""
33 |
34 | #: admin.py:136 admin.py:238
35 | #, python-format
36 | msgid "The %(name)s \"%(obj)s\" was changed successfully."
37 | msgstr ""
38 |
39 | #: admin.py:211
40 | #, python-format
41 | msgid "Relation %(rel)s could not be found in the model %(model)s."
42 | msgstr ""
43 |
44 | #: admin.py:216
45 | #, python-format
46 | msgid "Relation %(rel)s in the model %(model)s is not many-to-one."
47 | msgstr ""
48 |
49 | #: models.py:11
50 | msgid "language"
51 | msgstr ""
52 |
53 | #: templates/admin/includes/ml_links.html:10
54 | msgid "Save and Add/Edit Translation:"
55 | msgstr ""
56 |
57 | #: tests/lang_prefixed_urls.py:14
58 | msgid "^LANG_CODE/"
59 | msgstr "^en/"
60 |
61 | #: tests/models.py:14
62 | msgid "publish date/time"
63 | msgstr ""
64 |
65 | #: tests/models.py:15
66 | msgid "public"
67 | msgstr ""
68 |
69 | #: tests/models.py:18
70 | msgid "news story"
71 | msgstr ""
72 |
73 | #: tests/models.py:19
74 | msgid "news stories"
75 | msgstr ""
76 |
77 | #: tests/models.py:36
78 | msgid "core news story"
79 | msgstr ""
80 |
81 | #: tests/models.py:37
82 | msgid "headline"
83 | msgstr ""
84 |
85 | #: tests/models.py:38
86 | msgid "slug"
87 | msgstr ""
88 |
89 | #: tests/models.py:39
90 | msgid "body"
91 | msgstr ""
92 |
93 | #: tests/models.py:42
94 | msgid "news story translation"
95 | msgstr ""
96 |
97 | #: tests/models.py:43
98 | msgid "news story translations"
99 | msgstr ""
100 |
101 | #: tests/settings.py:11
102 | msgid "English"
103 | msgstr ""
104 |
105 | #: tests/settings.py:12
106 | msgid "French"
107 | msgstr ""
108 |
109 | #: tests/settings.py:13
110 | msgid "German"
111 | msgstr ""
112 |
113 | #: tests/spangles_urls.py:10
114 | msgid "^trans-stripes/"
115 | msgstr ""
116 |
117 | #: tests/urls.py:10
118 | msgid "^trans-things/$"
119 | msgstr ""
120 |
121 | #: tests/urls.py:11
122 | msgid "^multi-module-spangles/"
123 | msgstr ""
124 |
125 | #: tests/urls.py:12
126 | msgid "^news-story/(?P[-\\w]+)/$"
127 | msgstr ""
128 |
129 | #: tests/templates/transurlvania_tests/stuff.html:2
130 | msgid "Blow up the earth? I hope not! It's where I keep all my stuff!"
131 | msgstr ""
132 |
--------------------------------------------------------------------------------
/transurlvania/locale/fr/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
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 , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2009-01-26 17:59-0500\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: FULL NAME \n"
14 | "Language-Team: LANGUAGE \n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 |
19 | #: admin.py:109
20 | #, python-format
21 | msgid "Field %(field)s could not be found in the model %(model)s."
22 | msgstr ""
23 |
24 | #: admin.py:114
25 | #, python-format
26 | msgid "Field %(field)s in the model %(model)s is not a ForeignKey."
27 | msgstr ""
28 |
29 | #: admin.py:122 admin.py:224
30 | #, python-format
31 | msgid "The %(name)s \"%(obj)s\" was added successfully."
32 | msgstr ""
33 |
34 | #: admin.py:136 admin.py:238
35 | #, python-format
36 | msgid "The %(name)s \"%(obj)s\" was changed successfully."
37 | msgstr ""
38 |
39 | #: admin.py:211
40 | #, python-format
41 | msgid "Relation %(rel)s could not be found in the model %(model)s."
42 | msgstr ""
43 |
44 | #: admin.py:216
45 | #, python-format
46 | msgid "Relation %(rel)s in the model %(model)s is not many-to-one."
47 | msgstr ""
48 |
49 | #: models.py:11
50 | msgid "language"
51 | msgstr ""
52 |
53 | #: templates/admin/includes/ml_links.html:10
54 | msgid "Save and Add/Edit Translation:"
55 | msgstr ""
56 |
57 | #: tests/lang_prefixed_urls.py:14
58 | msgid "^LANG_CODE/"
59 | msgstr "^fr/"
60 |
61 | #: tests/models.py:14
62 | msgid "publish date/time"
63 | msgstr ""
64 |
65 | #: tests/models.py:15
66 | msgid "public"
67 | msgstr ""
68 |
69 | #: tests/models.py:18
70 | msgid "news story"
71 | msgstr ""
72 |
73 | #: tests/models.py:19
74 | msgid "news stories"
75 | msgstr ""
76 |
77 | #: tests/models.py:36
78 | msgid "core news story"
79 | msgstr ""
80 |
81 | #: tests/models.py:37
82 | msgid "headline"
83 | msgstr ""
84 |
85 | #: tests/models.py:38
86 | msgid "slug"
87 | msgstr ""
88 |
89 | #: tests/models.py:39
90 | msgid "body"
91 | msgstr ""
92 |
93 | #: tests/models.py:42
94 | msgid "news story translation"
95 | msgstr ""
96 |
97 | #: tests/models.py:43
98 | msgid "news story translations"
99 | msgstr ""
100 |
101 | #: tests/settings.py:11
102 | msgid "English"
103 | msgstr ""
104 |
105 | #: tests/settings.py:12
106 | msgid "French"
107 | msgstr ""
108 |
109 | #: tests/settings.py:13
110 | msgid "German"
111 | msgstr ""
112 |
113 | #: tests/spangles_urls.py:10
114 | msgid "^trans-stripes/"
115 | msgstr "^trans-bandes/"
116 |
117 | #: tests/urls.py:10
118 | msgid "^trans-things/$"
119 | msgstr "^trans-chose/$"
120 |
121 | #: tests/urls.py:11
122 | msgid "^multi-module-spangles/"
123 | msgstr "^module-multi-de-spangles/"
124 |
125 | #: tests/urls.py:12
126 | msgid "^news-story/(?P[-\\w]+)/$"
127 | msgstr "^nouvelle/(?P[-\\w]+)/$"
128 |
129 | #: tests/templates/transurlvania_tests/stuff.html:2
130 | msgid "Blow up the earth? I hope not! It's where I keep all my stuff!"
131 | msgstr ""
132 |
--------------------------------------------------------------------------------
/transurlvania/middleware.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.core import urlresolvers
3 | from django.utils import translation
4 |
5 | from transurlvania.settings import LANGUAGE_DOMAINS
6 | from transurlvania.translators import URLTranslator, AutodetectScheme
7 |
8 |
9 | class LangInPathMiddleware(object):
10 | """
11 | Middleware for determining site's language via a language code in the path
12 | This needs to be installed after the LocaleMiddleware so it can override
13 | that middleware's decisions.
14 | """
15 | def __init__(self):
16 | self.lang_codes = set(dict(settings.LANGUAGES).keys())
17 |
18 | def process_request(self, request):
19 | potential_lang_code = request.path_info.lstrip('/').split('/', 1)[0]
20 | if potential_lang_code in self.lang_codes:
21 | translation.activate(potential_lang_code)
22 | request.LANGUAGE_CODE = translation.get_language()
23 |
24 |
25 | class LangInDomainMiddleware(object):
26 | """
27 | Middleware for determining site's language via the domain name used in
28 | the request.
29 | This needs to be installed after the LocaleMiddleware so it can override
30 | that middleware's decisions.
31 | """
32 |
33 | def process_request(self, request):
34 | for lang in LANGUAGE_DOMAINS.keys():
35 | if LANGUAGE_DOMAINS[lang][0] == request.META['SERVER_NAME']:
36 | translation.activate(lang)
37 | request.LANGUAGE_CODE = translation.get_language()
38 |
39 |
40 | class URLTransMiddleware(object):
41 | def process_request(self, request):
42 | request.url_translator = URLTranslator(request.build_absolute_uri())
43 |
44 | def process_view(self, request, view_func, view_args, view_kwargs):
45 | request.url_translator.set_view_info(view_func, view_args, view_kwargs)
46 | request.url_translator.scheme = AutodetectScheme()
47 | return None
48 |
49 |
50 | class BlockLocaleMiddleware(object):
51 | """
52 | This middleware will prevent users from accessing the site in a specified
53 | list of languages unless the user is authenticated and a staff member.
54 | It should be installed below LocaleMiddleware and AuthenticationMiddleware.
55 | """
56 | def __init__(self):
57 | self.default_lang = settings.LANGUAGE_CODE
58 | self.blocked_langs = set(getattr(settings, 'BLOCKED_LANGUAGES', []))
59 |
60 | def process_request(self, request):
61 | lang = getattr(request, 'LANGUAGE_CODE', None)
62 | if lang in self.blocked_langs and (not hasattr(request, 'user') or not request.user.is_staff):
63 | request.LANGUAGE_CODE = self.default_lang
64 | translation.activate(self.default_lang)
65 |
66 |
67 | class URLCacheResetMiddleware(object):
68 | """
69 | Middleware that resets the URL resolver cache after each response.
70 |
71 | Install this as the first middleware in the list so it gets run last as the
72 | response goes out. It will clear the URLResolver cache. The cache needs to
73 | be cleared between requests because the URLResolver objects in the cache
74 | are locked into one language, and the next request might be in a different
75 | language.
76 |
77 | This middleware is required if the project uses translated URLs.
78 | """
79 | def process_response(self, request, response):
80 | urlresolvers.clear_url_caches()
81 | return response
82 |
--------------------------------------------------------------------------------
/transurlvania/templatetags/transurlvania_tags.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.template.defaultfilters import stringfilter
3 |
4 | from django.utils.translation import check_for_language
5 | from django.utils.translation.trans_real import translation
6 |
7 | from transurlvania.translators import NoTranslationError
8 |
9 |
10 | register = template.Library()
11 |
12 |
13 | def token_splitter(token, unquote=False):
14 | """
15 | Splits a template tag token and returns a dict containing:
16 | * tag_name - The template tag name (ie. first piece in the token)
17 | * args - List of arguments to the template tag
18 | * context_var - Context variable name if the "as" keyword is used,
19 | or None otherwise.
20 | """
21 | pieces = token.split_contents()
22 |
23 | tag_name = pieces[0]
24 | args = []
25 | context_var = None
26 | if len(pieces) > 2 and pieces[-2] == 'as':
27 | args = pieces[1:-2]
28 | context_var = pieces[-1]
29 | elif len(pieces) > 1:
30 | args = pieces[1:]
31 |
32 | if unquote:
33 | args = [strip_quotes(arg) for arg in args]
34 |
35 | return {
36 | 'tag_name':tag_name,
37 | 'args':args,
38 | 'context_var':context_var,
39 | }
40 |
41 |
42 | def strip_quotes(string):
43 | """
44 | Strips quotes off the ends of `string` if it is quoted
45 | and returns it.
46 | """
47 | if len(string) >= 2:
48 | if string[0] == string[-1]:
49 | if string[0] in ('"', "'"):
50 | string = string[1:-1]
51 | return string
52 |
53 |
54 | @register.tag
55 | def this_page_in_lang(parser, token):
56 | """
57 | Returns the URL for the equivalent of the current page in the requested language.
58 | If no URL can be generated, returns nothing.
59 |
60 | Usage:
61 |
62 | {% this_page_in_lang "fr" %}
63 | {% this_page_in_lang "fr" as var_name %}
64 |
65 | """
66 | bits = token_splitter(token)
67 | fallback = None
68 | if len(bits['args']) < 1:
69 | raise template.TemplateSyntaxError, "%s tag requires at least one argument" % bits['tag_name']
70 | elif len(bits['args']) == 2:
71 | fallback = bits['args'][1]
72 | elif len(bits['args']) > 2:
73 | raise template.TemplateSyntaxError, "%s tag takes at most two arguments" % bits['tag_name']
74 |
75 | return ThisPageInLangNode(bits['args'][0], fallback, bits['context_var'])
76 |
77 |
78 | class ThisPageInLangNode(template.Node):
79 | def __init__(self, lang, fallback=None, context_var=None):
80 | self.context_var = context_var
81 | self.lang = template.Variable(lang)
82 | if fallback:
83 | self.fallback = template.Variable(fallback)
84 | else:
85 | self.fallback = None
86 |
87 | def render(self, context):
88 | try:
89 | output = context['_url_translator'].get_url(
90 | self.lang.resolve(context), context
91 | )
92 | except (KeyError, NoTranslationError), e:
93 | output = ''
94 |
95 | if (not output) and self.fallback:
96 | output = self.fallback.resolve(context)
97 |
98 | if self.context_var:
99 | context[self.context_var] = output
100 | return ''
101 | else:
102 | return output
103 |
104 |
105 | @register.filter
106 | @stringfilter
107 | def trans_in_lang(string, lang):
108 | """
109 | Translate a string into a specific language (which can be different
110 | than the set language).
111 |
112 | Usage:
113 |
114 | {{ var|trans_in_lang:"fr" }}
115 |
116 | """
117 | if check_for_language(lang):
118 | return translation(lang).ugettext(string)
119 | return string
120 |
--------------------------------------------------------------------------------
/transurlvania/translators.py:
--------------------------------------------------------------------------------
1 | from django.core.urlresolvers import NoReverseMatch
2 |
3 | from transurlvania.urlresolvers import reverse_for_language
4 |
5 |
6 | class NoTranslationError(Exception):
7 | pass
8 |
9 |
10 | class ViewInfo(object):
11 | def __init__(self, current_url, view_func, view_args, view_kwargs):
12 | self.current_url = current_url
13 | self.view_func = view_func
14 | self.view_args = view_args
15 | self.view_kwargs = view_kwargs
16 |
17 | def __unicode__(self):
18 | return 'URL:%s, handled by %s(*%s, **%s)' % (self.current_url,
19 | self.view_func, self.view_args, self.view_kwargs)
20 |
21 | class BasicScheme(object):
22 | def get_url(self, lang, view_info, context=None):
23 | "The basic translation scheme just returns the current URL"
24 | return view_info.current_url
25 |
26 |
27 | class ObjectBasedScheme(BasicScheme):
28 | """
29 | Translates by finding the specified object in the context dictionary,
30 | getting that object's translation in teh requested language, and
31 | returning the object's URL.
32 | """
33 |
34 | DEFAULT_OBJECT_NAME = 'object'
35 |
36 | def __init__(self, object_name=None):
37 | self.object_name = object_name or self.DEFAULT_OBJECT_NAME
38 |
39 | def get_url(self, lang, view_info, context=None):
40 | try:
41 | return context[self.object_name].get_translation(lang).get_absolute_url()
42 | except KeyError:
43 | raise NoTranslationError(u'Could not find object named %s in context.' % self.object_name)
44 | except AttributeError:
45 | raise NoTranslationError(u'Unable to get translation of object %s '
46 | u'in language %s' % (context[self.object_name], lang))
47 |
48 |
49 | class DirectToURLScheme(BasicScheme):
50 | """
51 | Translates using a view function (or URL name) and the args and kwargs that
52 | need to be passed to it. The URL is found by doing a reverse lookup for the
53 | specified view in the requested language.
54 | """
55 |
56 | def __init__(self, url_name=None):
57 | self.url_name = url_name
58 |
59 | def get_url(self, lang, view_info, context=None):
60 | view_func = self.url_name or view_info.view_func
61 | try:
62 | return reverse_for_language(view_func, lang, None,
63 | view_info.view_args, view_info.view_kwargs)
64 | except NoReverseMatch:
65 | raise NoTranslationError('Unable to find URL for %s' % view_func)
66 |
67 |
68 | class AutodetectScheme(BasicScheme):
69 | """
70 | Tries to translate using an "object" entry in the context, or, failing
71 | that, tries to find the URL for the view function it was given in the
72 | requested language.
73 | """
74 | def __init__(self, object_name=None):
75 | self.object_translator = ObjectBasedScheme(object_name)
76 | self.view_translator = DirectToURLScheme()
77 |
78 | def get_url(self, lang, view_info, context=None):
79 | """
80 | Tries translating with the object based scheme and falls back to the
81 | direct-to-URL based scheme if that fails.
82 | """
83 | try:
84 | return self.object_translator.get_url(lang, view_info, context)
85 | except NoTranslationError:
86 | try:
87 | return self.view_translator.get_url(lang, view_info, context)
88 | except NoTranslationError:
89 | return super(AutodetectScheme, self).get_url(lang, view_info, context)
90 |
91 |
92 | class URLTranslator(object):
93 | def __init__(self, current_url, scheme=None):
94 | self.scheme = scheme or BasicScheme()
95 | self.view_info = ViewInfo(current_url, None, None, None)
96 |
97 | def set_view_info(self, view_func, view_args, view_kwargs):
98 | self.view_info.view_func = view_func
99 | self.view_info.view_args = view_args
100 | self.view_info.view_kwargs = view_kwargs
101 |
102 | def __unicode__(self):
103 | return 'URL Translator for %s. Using scheme: %s.' % (self.view_info,
104 | self.scheme)
105 |
106 | def get_url(self, lang, context=None):
107 | return self.scheme.get_url(lang, self.view_info, context)
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Multilang URLs
2 | ==============
3 |
4 | Introduction
5 | ------------
6 |
7 | This application provides a collection of URL-related utilities for
8 | multi-lingual projects.
9 |
10 | Features
11 | --------
12 |
13 | * **Localizable URLs** - the same page can have a different url in a different
14 | language. (eg /products/ can be /produits/ in French)
15 |
16 | * **Language-in-Path** - a replacement for Django's language cookie that
17 | makes URLs language-specific by storing the language code in the URL path.
18 |
19 | * **Language-in-Domain** - a replacement for Django's language cookie that
20 | makes URLs language-specific by mapping each domain for the site onto a
21 | language.
22 |
23 |
24 | Installation
25 | ------------
26 |
27 | * Add ``transurlvania`` to ``INSTALLED_APPS`` in your settings file
28 |
29 | * Add the following middlewares to ``MIDDLEWARE_CLASSES`` in your settings file:
30 |
31 | * ``transurlvania.middleware.URLCacheResetMiddleware`` (must be before the
32 | ``SessionMiddleware``)
33 |
34 | * ``transurlvania.middleware.URLTransMiddleware`` (must be before the
35 | ``CommonMiddleware`` in order for APPEND_SLASH to work)
36 |
37 | * Add ``transurlvania.context_processors.translate`` to
38 | ``TEMPLATE_CONTEXT_PROCESSORS``.
39 |
40 | Usage
41 | -----
42 |
43 | Localizing URLs
44 | ~~~~~~~~~~~~~~~
45 |
46 | Replace the usual::
47 |
48 | from django.conf.urls.defaults import *
49 |
50 | with::
51 |
52 | from transurlvania.defaults import *
53 |
54 | You will need the ugettext_noop function if you want to mark any URL patterns
55 | for localization::
56 |
57 | from django.utils.translation import ugettext_noop as _
58 |
59 | To make an URL pattern localizable, first ensure that it is in the
60 | ``url(...)`` form, then wrap the URL pattern itself in a gettext function
61 | call::
62 |
63 | url(_(r'^about-us/$'), 'about_us', name='about_us'),
64 |
65 | Now, when you next run the ``makemessages`` management command, these URL
66 | patterns will be collected in the .po file along with all the other
67 | localizable strings.
68 |
69 | Notes:
70 |
71 | * because the strings in the po file are not raw strings, some regex
72 | characters will be escaped, so the URL patterns are sometimes less readable
73 |
74 | * When providing a translation for a URL pattern that includes regex elements,
75 | ensure that the translation contains the same regex elements, otherwise the
76 | pattern matching behaviour may vary from language to language.
77 |
78 | Localizing ``get_absolute_url``
79 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
80 |
81 | Any language-aware models that define ``get_absolute_url`` should decorate it
82 | with ``permalink_in_lang``, from ``transurlvania.decorators`` so that the
83 | returned URLs will be properly translated to the language of the object.
84 | ``permalink_in_lang`` accepts the same tuple values as ``permalink`` except
85 | that the language code to be used for the URL should be inserted between the
86 | name of the view or URL and the ``view_args`` parameter::
87 |
88 | @permalink_in_lang
89 | def get_absolute_url(self):
90 | ('name_of_view_or_url', self.language, (), {})
91 |
92 |
93 | Making URLs Language-Specific
94 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
95 |
96 | transurlvania provides two ways for making URLs language-specific, meaning that
97 | the URL itself will indicate what language to use when generating the
98 | response.
99 |
100 | Language in Path
101 | ````````````````
102 |
103 | * Add ``transurlvania.middleware.LangInPathMiddleware`` to ``MIDDLEWARE_CLASSES``
104 | after ``LocaleMiddleware``.
105 |
106 | * Make these changes to your root URL conf module:
107 |
108 | * If you haven't already done so, replace
109 | ``from django.conf.urls.defaults import *`` with
110 | ``from transurlvania.defaults import *``.
111 |
112 | * Replace the call to ``patterns`` that populates the ``urlpatterns``
113 | variable with a call to ``lang_prefixed_patterns``.
114 |
115 | * To handle requests to the root URL itself ("/") or any URLs you wish to
116 | keep outside of the language prefixing, declare the URL patterns as
117 | normal inside a call to ``patterns`` and append the result to the
118 | ``urlpatterns`` variable.
119 |
120 | Here's an example of what a root URLconf might look like before adding
121 | language prefixing::
122 |
123 | from django.contrib import admin
124 | from django.utils.translation import ugettext_noop as _
125 |
126 | from transurlvania.defaults import *
127 |
128 | admin.autodiscover()
129 |
130 | urlpatterns = patterns('example.views',
131 | url(r'^$', 'home'),
132 | url(r'^admin/', include(admin.site.urls)),
133 | url(_(r'^about-us/$'), 'about_us', name='about_us'),
134 | )
135 |
136 | And here's what it would look like after it's been converted::
137 |
138 | from django.contrib import admin
139 | from django.utils.translation import ugettext_noop as _
140 |
141 | from transurlvania.defaults import *
142 |
143 | admin.autodiscover()
144 |
145 | urlpatterns = lang_prefixed_patterns('example.views',
146 | url(r'^$', 'home'),
147 | url(r'^admin/', include(admin.site.urls)),
148 | url(_(r'^about-us/$'), 'about_us', name='about_us'),
149 | )
150 |
151 |
152 | urlpatterns += patterns('example.views',
153 | url(r'^$', 'language_selection_splash'),
154 | )
155 |
156 | Language in Domain
157 | ``````````````````
158 |
159 | * Add ``transurlvania.middleware.LangInDomainMiddleware`` to ``MIDDLEWARE_CLASSES``
160 | after ``LocaleMiddleware``.
161 |
162 | * Add ``MULTILANG_LANGUAGE_DOMAINS`` to the project's settings module.
163 |
164 | This settings should be a dictionary mapping language codes to two-element
165 | tuples, where the first element is the domain for that language, and the
166 | second element is the name of the site this represents.
167 |
168 | Example::
169 |
170 | MULTILANG_LANGUAGE_DOMAINS = {
171 | 'en': ('www.example-en.com', 'English Site'),
172 | 'fr': ('www.example-fr.com', 'French Site')
173 | }
174 |
175 |
176 | Language Switching
177 | ``````````````````
178 |
179 | Django's language switching view is incompatible with transurlvania's
180 | techniques for setting site language using the URL. transurlvania provides its
181 | own language switching tools that make it possible to link directly to the
182 | loaded page's alternate-language equivalent.
183 |
184 | The main requirement for this functionality is that
185 | ``transurlvania.middleware.URLTransMiddleware`` is in ``MIDDLEWARE_CLASSES``, and
186 | ``transurlvania.context_processors.translate`` is in
187 | ``TEMPLATE_CONTEXT_PROCESSORS``. With these installed you can then use the
188 | ``this_page_in_lang`` template tag to get the URL for the page currently being
189 | viewed in the language requested.
190 |
191 | So, ``{% this_page_in_lang "fr" %}`` would return the URL to the French
192 | version of the page being displayed.
193 |
194 | The language switching code has two schemes for determining the URL to use:
195 |
196 | 1. If there's a variable named ``object`` in the context, and that variable
197 | implements a method named ``get_translation``, the switcher will call the
198 | method with the requsted language, call ``get_absolute_url`` on what's
199 | returned and then use that URL for the translation.
200 |
201 | 2. If the first method fails, the switcher will call transurlvania's
202 | reverse_for_language function using the view name and the parameters that were
203 | resolved from the current request.
204 |
205 | There are cases where neither of these schemes will work such as when the
206 | object isn't named ``object``, or when the same view is used by multiple URLs.
207 | In those cases, you can use the decorators provided by the ``translators``
208 | module to decorate the view and change which URL look-up scheme is used. You
209 | can also define your own look-up schemes.
210 |
211 | Language Based Blocking
212 | ~~~~~~~~~~~~~~~~~~~~~~~
213 |
214 | The ``BlockLocaleMiddleware`` will block non-admins from accessing the site in any language
215 | listed in the ``BLOCKED_LANGUAGES`` setting in the settings file.
216 |
--------------------------------------------------------------------------------
/transurlvania/urlresolvers.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from django.conf import settings
4 | from django.conf.urls.defaults import handler404, handler500
5 | from django.core.urlresolvers import RegexURLPattern, RegexURLResolver, get_callable
6 | from django.core.urlresolvers import NoReverseMatch
7 | from django.core.urlresolvers import get_script_prefix
8 | from django.utils.datastructures import MultiValueDict
9 | from django.utils.encoding import iri_to_uri, force_unicode
10 | from django.utils.regex_helper import normalize
11 | from django.utils.translation import get_language
12 | from django.utils.translation.trans_real import translation
13 |
14 | import transurlvania.settings
15 |
16 |
17 | _resolvers = {}
18 | def get_resolver(urlconf, lang):
19 | if urlconf is None:
20 | from django.conf import settings
21 | urlconf = settings.ROOT_URLCONF
22 | key = (urlconf, lang)
23 | if key not in _resolvers:
24 | _resolvers[key] = MultilangRegexURLResolver(r'^/', urlconf)
25 | return _resolvers[key]
26 |
27 |
28 | def reverse_for_language(viewname, lang, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None):
29 | # Based on code in Django 1.1.1 in reverse and RegexURLResolver.reverse
30 | # in django.core.urlresolvers.
31 | args = args or []
32 | kwargs = kwargs or {}
33 | if prefix is None:
34 | prefix = get_script_prefix()
35 | resolver = get_resolver(urlconf, lang)
36 |
37 | if not isinstance(viewname, basestring):
38 | view = viewname
39 | else:
40 | parts = viewname.split(':')
41 | parts.reverse()
42 | view = parts[0]
43 | path = parts[1:]
44 |
45 | resolved_path = []
46 | while path:
47 | ns = path.pop()
48 |
49 | # Lookup the name to see if it could be an app identifier
50 | try:
51 | app_list = resolver.app_dict[ns]
52 | # Yes! Path part matches an app in the current Resolver
53 | if current_app and current_app in app_list:
54 | # If we are reversing for a particular app, use that namespace
55 | ns = current_app
56 | elif ns not in app_list:
57 | # The name isn't shared by one of the instances (i.e., the default)
58 | # so just pick the first instance as the default.
59 | ns = app_list[0]
60 | except KeyError:
61 | pass
62 |
63 | try:
64 | extra, resolver = resolver.namespace_dict[ns]
65 | resolved_path.append(ns)
66 | prefix = prefix + extra
67 | except KeyError, key:
68 | if resolved_path:
69 | raise NoReverseMatch("%s is not a registered namespace inside '%s'" % (key, ':'.join(resolved_path)))
70 | else:
71 | raise NoReverseMatch("%s is not a registered namespace" % key)
72 |
73 | if args and kwargs:
74 | raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
75 | try:
76 | lookup_view = get_callable(view, True)
77 | except (ImportError, AttributeError), e:
78 | raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
79 | if hasattr(resolver, 'get_reverse_dict'):
80 | possibilities = resolver.get_reverse_dict(lang).getlist(lookup_view)
81 | else:
82 | possibilities = resolver.reverse_dict.getlist(lookup_view)
83 | for possibility, pattern in possibilities:
84 | for result, params in possibility:
85 | if args:
86 | if len(args) != len(params):
87 | continue
88 | unicode_args = [force_unicode(val) for val in args]
89 | candidate = result % dict(zip(params, unicode_args))
90 | else:
91 | if set(kwargs.keys()) != set(params):
92 | continue
93 | unicode_kwargs = dict([(k, force_unicode(v)) for (k, v) in kwargs.items()])
94 | candidate = result % unicode_kwargs
95 | if re.search(u'^%s' % pattern, candidate, re.UNICODE):
96 | iri = u'%s%s' % (prefix, candidate)
97 | # If we have a separate domain for lang, put that in the iri
98 | domain = transurlvania.settings.LANGUAGE_DOMAINS.get(lang, None)
99 | if domain:
100 | iri = u'http://%s%s' % (domain[0], iri)
101 | return iri_to_uri(iri)
102 | # lookup_view can be URL label, or dotted path, or callable, Any of
103 | # these can be passed in at the top, but callables are not friendly in
104 | # error messages.
105 | m = getattr(lookup_view, '__module__', None)
106 | n = getattr(lookup_view, '__name__', None)
107 | if m is not None and n is not None:
108 | lookup_view_s = "%s.%s" % (m, n)
109 | else:
110 | lookup_view_s = lookup_view
111 | raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
112 | "arguments '%s' not found." % (lookup_view_s, args, kwargs))
113 |
114 |
115 | class MultilangRegexURLPattern(RegexURLPattern):
116 | def __init__(self, regex, callback, default_args=None, name=None):
117 | # Copied from django.core.urlresolvers.RegexURLPattern, with one change:
118 | # The regex here is stored as a string instead of as a compiled re object.
119 | # This allows the code to use the gettext system to translate the URL
120 | # pattern at resolve time.
121 | self._raw_regex = regex
122 |
123 | if callable(callback):
124 | self._callback = callback
125 | else:
126 | self._callback = None
127 | self._callback_str = callback
128 | self.default_args = default_args or {}
129 | self.name = name
130 | self._regex_dict = {}
131 |
132 | def get_regex(self, lang=None):
133 | lang = lang or get_language()
134 | return self._regex_dict.setdefault(lang, re.compile(translation(lang).ugettext(self._raw_regex), re.UNICODE))
135 | regex = property(get_regex)
136 |
137 |
138 | class MultilangRegexURLResolver(RegexURLResolver):
139 | def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
140 | # regex is a string representing a regular expression.
141 | # urlconf_name is a string representing the module containing urlconfs.
142 | self._raw_regex = regex
143 | self.urlconf_name = urlconf_name
144 | if not isinstance(urlconf_name, basestring):
145 | self._urlconf_module = self.urlconf_name
146 | self.callback = None
147 | self.default_kwargs = default_kwargs or {}
148 | self.namespace = namespace
149 | self.app_name = app_name
150 | self._lang_reverse_dicts = {}
151 | self._namespace_dict = None
152 | self._app_dict = None
153 | self._regex_dict = {}
154 |
155 | def get_regex(self, lang=None):
156 | lang = lang or get_language()
157 | # Only attempt to get the translation of the regex if the regex string
158 | # is not empty. The empty string is handled as a special case by
159 | # Django's gettext. It's where it stores its metadata.
160 | if self._raw_regex != '':
161 | regex_in_lang = translation(lang).ugettext(self._raw_regex)
162 | else:
163 | regex_in_lang = self._raw_regex
164 | return self._regex_dict.setdefault(lang, re.compile(regex_in_lang, re.UNICODE))
165 | regex = property(get_regex)
166 |
167 | def _build_reverse_dict_for_lang(self, lang):
168 | reverse_dict = MultiValueDict()
169 | namespaces = {}
170 | apps = {}
171 | for pattern in reversed(self.url_patterns):
172 | if hasattr(pattern, 'get_regex'):
173 | p_pattern = pattern.get_regex(lang).pattern
174 | else:
175 | p_pattern = pattern.regex.pattern
176 | if p_pattern.startswith('^'):
177 | p_pattern = p_pattern[1:]
178 | if isinstance(pattern, RegexURLResolver):
179 | if pattern.namespace:
180 | namespaces[pattern.namespace] = (p_pattern, pattern)
181 | if pattern.app_name:
182 | apps.setdefault(pattern.app_name, []).append(pattern.namespace)
183 | else:
184 | if hasattr(pattern, 'get_regex'):
185 | parent = normalize(pattern.get_regex(lang).pattern)
186 | else:
187 | parent = normalize(pattern.regex.pattern)
188 | if hasattr(pattern, 'get_reverse_dict'):
189 | sub_reverse_dict = pattern.get_reverse_dict(lang)
190 | else:
191 | sub_reverse_dict = pattern.reverse_dict
192 | for name in sub_reverse_dict:
193 | for matches, pat in sub_reverse_dict.getlist(name):
194 | new_matches = []
195 | for piece, p_args in parent:
196 | new_matches.extend([(piece + suffix, p_args + args) for (suffix, args) in matches])
197 | reverse_dict.appendlist(name, (new_matches, p_pattern + pat))
198 | for namespace, (prefix, sub_pattern) in pattern.namespace_dict.items():
199 | namespaces[namespace] = (p_pattern + prefix, sub_pattern)
200 | for app_name, namespace_list in pattern.app_dict.items():
201 | apps.setdefault(app_name, []).extend(namespace_list)
202 | else:
203 | bits = normalize(p_pattern)
204 | reverse_dict.appendlist(pattern.callback, (bits, p_pattern))
205 | reverse_dict.appendlist(pattern.name, (bits, p_pattern))
206 | self._namespace_dict = namespaces
207 | self._app_dict = apps
208 | return reverse_dict
209 |
210 | def get_reverse_dict(self, lang=None):
211 | if lang is None:
212 | lang = get_language()
213 | if lang not in self._lang_reverse_dicts:
214 | self._lang_reverse_dicts[lang] = self._build_reverse_dict_for_lang(lang)
215 | return self._lang_reverse_dicts[lang]
216 | reverse_dict = property(get_reverse_dict)
217 |
218 |
219 | class LangSelectionRegexURLResolver(MultilangRegexURLResolver):
220 | def __init__(self, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
221 | # urlconf_name is a string representing the module containing urlconfs.
222 | self.urlconf_name = urlconf_name
223 | if not isinstance(urlconf_name, basestring):
224 | self._urlconf_module = self.urlconf_name
225 | self.callback = None
226 | self.default_kwargs = default_kwargs or {}
227 | self.namespace = namespace
228 | self.app_name = app_name
229 | self._lang_reverse_dicts = {}
230 | self._namespace_dict = None
231 | self._app_dict = None
232 | self._regex_dict = {}
233 |
234 | def get_regex(self, lang=None):
235 | lang = lang or get_language()
236 | return re.compile('^%s/' % lang)
237 | regex = property(get_regex)
238 |
239 |
240 | class PocketURLModule(object):
241 | handler404 = handler404
242 | handler500 = handler500
243 |
244 | def __init__(self, pattern_list):
245 | self.urlpatterns = pattern_list
246 |
247 |
--------------------------------------------------------------------------------
/tests/garfield/tests.py:
--------------------------------------------------------------------------------
1 | #encoding=utf-8
2 | import re
3 |
4 | from django.conf import settings
5 | from django.contrib.auth.models import User
6 | from django.core.exceptions import ImproperlyConfigured
7 | from django.core.urlresolvers import get_resolver, reverse, clear_url_caches
8 | from django.core.urlresolvers import NoReverseMatch
9 | from django.template import Context, Template, TemplateSyntaxError
10 | from django.test import TestCase, Client
11 | from django.utils import translation, http
12 |
13 | import transurlvania.settings
14 | from transurlvania import urlresolvers as transurlvania_resolvers
15 | from transurlvania.translators import NoTranslationError
16 | from transurlvania.urlresolvers import reverse_for_language
17 | from transurlvania.utils import complete_url
18 | from transurlvania.views import detect_language_and_redirect
19 |
20 | from garfield.views import home, about_us, the_president
21 | from garfield.views import comic_strip_list, comic_strip_detail, landing
22 | from garfield.views import jim_davis
23 |
24 |
25 | french_version_anchor_re = re.compile(r'')
26 |
27 |
28 | class TransURLTestCase(TestCase):
29 | """
30 | Test the translatable URL functionality
31 | These tests require that English (en) and French (fr) are both listed in
32 | the LANGUAGES list in settings.
33 | """
34 | def setUp(self):
35 | translation.activate('en')
36 |
37 | def tearDown(self):
38 | translation.deactivate()
39 | clear_url_caches()
40 |
41 | def testNormalURL(self):
42 | self.assertEqual(get_resolver(None).resolve('/en/garfield/')[0], landing)
43 | translation.activate('fr')
44 | self.assertEqual(get_resolver(None).resolve('/fr/garfield/')[0], landing)
45 |
46 | def testTransMatches(self):
47 | self.assertEqual(get_resolver(None).resolve('/en/about-us/')[0], about_us)
48 | translation.activate('fr')
49 | self.assertEqual(get_resolver(None).resolve('/fr/a-propos-de-nous/')[0], about_us)
50 |
51 | def testMultiModuleMixedURL(self):
52 | self.assertEqual(get_resolver(None).resolve('/en/garfield/jim-davis/')[0], jim_davis)
53 | translation.activate('fr')
54 | self.assertEqual(get_resolver(None).resolve('/fr/garfield/jim-davis/')[0], jim_davis)
55 |
56 | def testMultiModuleTransURL(self):
57 | self.assertEqual(get_resolver(None).resolve(u'/en/garfield/the-president/')[0], the_president)
58 | translation.activate('fr')
59 | self.assertEqual(get_resolver(None).resolve(u'/fr/garfield/le-président/')[0], the_president)
60 |
61 | def testRootURLReverses(self):
62 | self.assertEqual(reverse(detect_language_and_redirect, 'tests.urls'), '/')
63 | translation.activate('fr')
64 | self.assertEqual(reverse(detect_language_and_redirect, 'tests.urls'), '/')
65 |
66 | def testNormalURLReverses(self):
67 | translation.activate('en')
68 | self.assertEqual(reverse(landing), '/en/garfield/')
69 | clear_url_caches()
70 | translation.activate('fr')
71 | self.assertEqual(reverse(landing), '/fr/garfield/')
72 |
73 | def testTransReverses(self):
74 | translation.activate('en')
75 | self.assertEqual(reverse(the_president), '/en/garfield/the-president/')
76 | # Simulate URLResolver cache reset between requests
77 | clear_url_caches()
78 | translation.activate('fr')
79 | self.assertEqual(reverse(the_president), http.urlquote(u'/fr/garfield/le-président/'))
80 |
81 | def testReverseForLangSupportsAdmin(self):
82 | try:
83 | reverse_for_language('admin:garfield_comicstrip_add', 'en')
84 | except NoReverseMatch, e:
85 | self.fail("Reverse lookup failed: %s" % e)
86 |
87 |
88 | class LangInPathTestCase(TestCase):
89 | """
90 | Test language setting via URL path
91 | LocaleMiddleware and LangInPathMiddleware must be listed in
92 | MIDDLEWARE_CLASSES for these tests to run properly.
93 | """
94 | def setUp(self):
95 | translation.activate('en')
96 |
97 | def tearDown(self):
98 | translation.deactivate()
99 |
100 | def testLangDetectionViewRedirectsToLang(self):
101 | self.client.cookies['django_language'] = 'de'
102 | response = self.client.get('/')
103 | self.assertRedirects(response, '/de/')
104 |
105 | def testNormalURL(self):
106 | response = self.client.get('/en/garfield/')
107 | self.assertEqual(response.status_code, 200)
108 | self.assertTemplateUsed(response, 'garfield/landing.html')
109 | self.assertEqual(response.context.get('LANGUAGE_CODE', None), 'en')
110 | response = self.client.get('/fr/garfield/')
111 | self.assertEqual(response.status_code, 200)
112 | self.assertTemplateUsed(response, 'garfield/landing.html')
113 | self.assertEqual(response.context.get('LANGUAGE_CODE', None), 'fr')
114 |
115 | def testTranslatedURL(self):
116 | response = self.client.get('/en/garfield/the-cat/')
117 | self.assertEqual(response.status_code, 200)
118 | self.assertTemplateUsed(response, 'garfield/comicstrip_list.html')
119 | self.assertEqual(response.context.get('LANGUAGE_CODE', None), 'en')
120 | response = self.client.get('/fr/garfield/le-chat/')
121 | self.assertEqual(response.status_code, 200)
122 | self.assertTemplateUsed(response, 'garfield/comicstrip_list.html')
123 | self.assertEqual(response.context.get('LANGUAGE_CODE', None), 'fr')
124 |
125 | def testReverseForLanguage(self):
126 | translation.activate('en')
127 | self.assertEquals(
128 | reverse_for_language(the_president, 'en'),
129 | '/en/garfield/the-president/'
130 | )
131 | self.assertEquals(
132 | reverse_for_language(the_president, 'fr'),
133 | http.urlquote('/fr/garfield/le-président/')
134 | )
135 |
136 | translation.activate('fr')
137 | self.assertEquals(
138 | reverse_for_language(the_president, 'fr'),
139 | http.urlquote('/fr/garfield/le-président/')
140 | )
141 | self.assertEquals(
142 | reverse_for_language(the_president, 'en'),
143 | '/en/garfield/the-president/'
144 | )
145 |
146 |
147 | class LangInDomainTestCase(TestCase):
148 | """
149 | Test language setting via URL path
150 | LangInDomainMiddleware must be listed in MIDDLEWARE_CLASSES for these tests
151 | to run properly.
152 | """
153 | urls = 'tests.urls_without_lang_prefix'
154 |
155 | def setUp(self):
156 | transurlvania.settings.LANGUAGE_DOMAINS = {
157 | 'en': ('www.trapeze-en.com', 'English Site'),
158 | 'fr': ('www.trapeze-fr.com', 'French Site')
159 | }
160 |
161 | def tearDown(self):
162 | translation.deactivate()
163 | transurlvania.settings.LANGUAGE_DOMAINS = {}
164 |
165 | def testRootURL(self):
166 | translation.activate('en')
167 | client = Client(SERVER_NAME='www.trapeze-fr.com')
168 | response = client.get('/')
169 | self.assertEqual(response.status_code, 200)
170 | self.assertEqual(response.context.get('LANGUAGE_CODE'), 'fr')
171 | transurlvania.settings.LANGUAGE_DOMAINS = {}
172 |
173 | translation.activate('fr')
174 | self.client = Client(SERVER_NAME='www.trapeze-en.com')
175 | response = self.client.get('/')
176 | self.assertEqual(response.status_code, 200)
177 | self.assertEqual(response.context.get('LANGUAGE_CODE'), 'en')
178 |
179 | def testURLWithPrefixes(self):
180 | translation.activate('en')
181 | response = self.client.get('/en/garfield/', SERVER_NAME='www.trapeze-fr.com')
182 | self.assertEqual(response.status_code, 404)
183 | response = self.client.get('/fr/garfield/', SERVER_NAME='www.trapeze-fr.com')
184 | self.assertEqual(response.context.get('LANGUAGE_CODE'), 'fr')
185 |
186 | translation.activate('fr')
187 | client = Client(SERVER_NAME='www.trapeze-en.com')
188 | response = client.get('/fr/garfield/')
189 | self.assertEqual(response.status_code, 404)
190 | response = client.get('/en/garfield/')
191 | self.assertEqual(response.context.get('LANGUAGE_CODE'), 'en')
192 |
193 | def testReverseForLangWithOneDifferentDomain(self):
194 | transurlvania.settings.LANGUAGE_DOMAINS = {
195 | 'fr': ('www.trapeze-fr.com', 'French Site')
196 | }
197 |
198 | fr_domain = transurlvania.settings.LANGUAGE_DOMAINS['fr'][0]
199 |
200 | translation.activate('en')
201 | self.assertEquals(reverse_for_language(about_us, 'en'), '/about-us/')
202 | self.assertEquals(
203 | reverse_for_language(about_us, 'fr'),
204 | u'http://%s/a-propos-de-nous/' % fr_domain
205 | )
206 |
207 | translation.activate('fr')
208 | self.assertEquals(
209 | reverse_for_language(about_us, 'fr'),
210 | u'http://%s/a-propos-de-nous/' % fr_domain
211 | )
212 | self.assertEquals(
213 | reverse_for_language(about_us, 'en'),
214 | '/about-us/'
215 | )
216 |
217 | def testBothDifferentDomains(self):
218 | transurlvania.settings.LANGUAGE_DOMAINS = {
219 | 'en': ('www.trapeze.com', 'English Site'),
220 | 'fr': ('www.trapeze-fr.com', 'French Site')
221 | }
222 |
223 | en_domain = transurlvania.settings.LANGUAGE_DOMAINS['en'][0]
224 | fr_domain = transurlvania.settings.LANGUAGE_DOMAINS['fr'][0]
225 |
226 | translation.activate('en')
227 | self.assertEquals(
228 | reverse_for_language(about_us, 'en', 'tests.urls'),
229 | 'http://%s/en/about-us/' % en_domain
230 | )
231 | self.assertEquals(
232 | reverse_for_language(about_us, 'fr', 'tests.urls'),
233 | 'http://%s/fr/a-propos-de-nous/' % fr_domain
234 | )
235 |
236 | translation.activate('fr')
237 | self.assertEquals(
238 | reverse_for_language(about_us, 'fr', 'tests.urls'),
239 | 'http://%s/fr/a-propos-de-nous/' % fr_domain
240 | )
241 | self.assertEquals(
242 | reverse_for_language(about_us, 'en', 'tests.urls'),
243 | 'http://%s/en/about-us/' % en_domain
244 | )
245 |
246 | def testDefaultViewBasedSwitchingWithSeparateDomains(self):
247 | transurlvania.settings.LANGUAGE_DOMAINS = {
248 | 'fr': ('www.trapeze-fr.com', 'French Site')
249 | }
250 |
251 | response = self.client.get('/about-us/')
252 | french_version_url = french_version_anchor_re.search(response.content).group(1)
253 | self.assertEqual(french_version_url,
254 | 'http://www.trapeze-fr.com/a-propos-de-nous/'
255 | )
256 |
257 |
258 |
259 | class LanguageSwitchingTestCase(TestCase):
260 | fixtures = ['test.json']
261 | """
262 | Test the language switching functionality of transurlvania (which also tests
263 | the `this_page_in_lang` template tag).
264 | """
265 | def tearDown(self):
266 | translation.deactivate()
267 |
268 | def testDefaultViewBasedSwitching(self):
269 | response = self.client.get('/en/about-us/')
270 | self.assertTemplateUsed(response, 'about_us.html')
271 | french_version_url = french_version_anchor_re.search(response.content).group(1)
272 | self.assertEqual(french_version_url, '/fr/a-propos-de-nous/')
273 |
274 | def testThisPageInLangTagWithFallBack(self):
275 |
276 | template = Template('{% load transurlvania_tags %}'
277 | '{% this_page_in_lang "fr" "/en/home/" %}'
278 | )
279 | output = template.render(Context({}))
280 | self.assertEquals(output, "/en/home/")
281 |
282 | def testThisPageInLangTagWithVariableFallBack(self):
283 | translation.activate('en')
284 | template = Template('{% load transurlvania_tags %}'
285 | '{% url garfield_landing as myurl %}'
286 | '{% this_page_in_lang "fr" myurl %}'
287 | )
288 | output = template.render(Context({}))
289 | self.assertEquals(output, '/en/garfield/')
290 |
291 | def testThisPageInLangTagNoArgs(self):
292 | try:
293 | template = Template('{% load transurlvania_tags %}'
294 | '{% this_page_in_lang %}'
295 | )
296 | except TemplateSyntaxError, e:
297 | self.assertEquals(unicode(e), u'this_page_in_lang tag requires at least one argument')
298 | else:
299 | self.fail()
300 |
301 | def testThisPageInLangTagExtraArgs(self):
302 | try:
303 | template = Template('{% load transurlvania_tags %}'
304 | '{% this_page_in_lang "fr" "/home/" "/sadf/" %}'
305 | )
306 | except TemplateSyntaxError, e:
307 | self.assertEquals(unicode(e), u'this_page_in_lang tag takes at most two arguments')
308 | else:
309 | self.fail()
310 |
311 | # TODO: Add tests for views that implement the view-based and object-based
312 | # translation schemes.
313 |
314 |
315 | class TransInLangTagTestCase(TestCase):
316 | """Tests for the `trans_in_lang` template tag."""
317 |
318 | def tearDown(self):
319 | translation.deactivate()
320 |
321 | def testBasic(self):
322 | """
323 | Tests the basic usage of the tag.
324 | """
325 | translation.activate('en')
326 | template_content = '{% load transurlvania_tags %}{% with "French" as myvar %}{{ myvar|trans_in_lang:"fr" }}{% endwith %}'
327 | template = Template(template_content)
328 | output = template.render(Context())
329 | self.assertEquals(output, u'Français')
330 |
331 | translation.activate('fr')
332 | template_content = '{% load transurlvania_tags %}{% with "French" as myvar %}{{ myvar|trans_in_lang:"en" }}{% endwith %}'
333 | template = Template(template_content)
334 | output = template.render(Context())
335 | self.assertEquals(output, u'French')
336 |
337 | def testVariableArgument(self):
338 | """
339 | Tests the tag when using a variable as the lang argument.
340 | """
341 | translation.activate('en')
342 | template_content = '{% load transurlvania_tags %}{% with "French" as myvar %}{% with "fr" as lang %}{{ myvar|trans_in_lang:lang }}{% endwith %}{% endwith %}'
343 | template = Template(template_content)
344 | output = template.render(Context())
345 | self.assertEquals(output, u'Français')
346 |
347 | def testKeepsPresetLanguage(self):
348 | """
349 | Tests that the tag does not change the language.
350 | """
351 | translation.activate('en')
352 | template_content = '{% load i18n %}{% load transurlvania_tags %}{% with "French" as myvar %}{{ myvar|trans_in_lang:"fr" }}|{% trans "French" %}{% endwith %}'
353 | template = Template(template_content)
354 | output = template.render(Context())
355 | self.assertEquals(output, u'Français|French')
356 |
357 | translation.activate('fr')
358 | template_content = '{% load i18n %}{% load transurlvania_tags %}{% with "French" as myvar %}{{ myvar|trans_in_lang:"en" }}|{% trans "French" %}{% endwith %}'
359 | template = Template(template_content)
360 | output = template.render(Context())
361 | self.assertEquals(output, u'French|Français')
362 |
363 | def testNoTranslation(self):
364 | """
365 | Tests the tag when there is no translation for the given string.
366 | """
367 | translation.activate('en')
368 | template_content = '{% load transurlvania_tags %}{% with "somethinginvalid" as myvar %}{{ myvar|trans_in_lang:"fr" }}{% endwith %}'
369 | template = Template(template_content)
370 | output = template.render(Context())
371 | self.assertEquals(output, u'somethinginvalid')
372 |
373 | def testRepeated(self):
374 | """
375 | Tests the tag when it is used repeatedly for different languages.
376 | """
377 | translation.activate('en')
378 | template_content = '{% load transurlvania_tags %}{% with "French" as myvar %}{{ myvar|trans_in_lang:"en" }}|{{ myvar|trans_in_lang:"fr" }}|{{ myvar|trans_in_lang:"de" }}{% endwith %}'
379 | template = Template(template_content)
380 | output = template.render(Context())
381 | self.assertEquals(output, u'French|Français|Französisch')
382 |
383 |
384 | def CompleteURLTestCase(TestCase):
385 | """
386 | Tests the `complete_url` utility function.
387 | """
388 | def tearDown(self):
389 | translation.deactivate()
390 |
391 | def testPath(self):
392 | translation.activate('en')
393 | self.assertEquals(complete_url('/path/'),
394 | 'http://www.trapeze-en.com/path/'
395 | )
396 | translation.activate('fr')
397 | self.assertEquals(complete_url('/path/'),
398 | 'http://www.trapeze-fr.com/path/'
399 | )
400 |
401 | def testFullUrl(self):
402 | translation.activate('fr')
403 | self.assertEquals(complete_url('http://www.google.com/path/'),
404 | 'http://www.google.com/path/'
405 | )
406 |
407 | def testNoDomain(self):
408 | translation.activate('de')
409 | self.assertRaises(ImproperlyConfigured, complete_url, '/path/')
410 |
411 | def testExplicitLang(self):
412 | translation.activate('en')
413 | self.assertEquals(complete_url('/path/', 'fr'),
414 | 'http://www.trapeze-fr.com/path/'
415 | )
416 | translation.activate('en')
417 | self.assertEquals(complete_url('/path/', 'en'),
418 | 'http://www.trapeze-en.com/path/'
419 | )
420 |
--------------------------------------------------------------------------------