├── 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 | 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 |
4 |
    5 | {% for lang in trans_links %} 6 |
  • {{ lang.name }}
  • 7 | {% endfor %} 8 |
9 | 10 | 11 | 12 | 13 | 14 | 19 |
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 | --------------------------------------------------------------------------------