├── example ├── example │ ├── __init__.py │ ├── context_processors.py │ ├── settings_test.py │ ├── urls.py │ ├── wsgi.py │ └── settings.py ├── tests │ ├── __init__.py │ ├── urls_noni18n.py │ ├── test_noni18n_urls.py │ ├── base.py │ └── test_solid_urls.py ├── templates │ ├── 404.html │ ├── about.html │ ├── onelang.html │ ├── home.html │ └── base.html ├── locale │ └── ru │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po └── manage.py ├── requirements.txt ├── requirements_test.txt ├── setup.cfg ├── pytest.ini ├── MANIFEST.in ├── .gitignore ├── solid_i18n ├── memory.py ├── __init__.py ├── contrib.py ├── urls.py ├── urlresolvers.py └── middleware.py ├── tox.ini ├── .travis.yml ├── LICENSE ├── setup.py ├── RELEASE_NOTES.md └── README.md /example/example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django>=1.8 2 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | pytest-django==2.8.0 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = example.settings_test 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include RELEASE_NOTES.md 3 | include README.rst 4 | include requirements.txt 5 | -------------------------------------------------------------------------------- /example/templates/404.html: -------------------------------------------------------------------------------- 1 |

Not Found

2 |

The requested URL {{ request_path }} was not found on this server.

3 | -------------------------------------------------------------------------------- /example/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st4lk/django-solid-i18n-urls/HEAD/example/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.TODO 3 | *.sublime-project 4 | *.sublime-workspace 5 | activate_venv*.bat 6 | .tox/ 7 | *.egg-info 8 | dist/ 9 | README.rst 10 | RELEASE_NOTES.rst 11 | generate_rst.py 12 | build/ 13 | .coverage 14 | .cache 15 | *.sqlite 16 | -------------------------------------------------------------------------------- /example/example/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def solid_i18n(request): 5 | example_vars = { 6 | 'SOLID_I18N_USE_REDIRECTS': settings.SOLID_I18N_USE_REDIRECTS, 7 | } 8 | return {"example_vars": example_vars} 9 | -------------------------------------------------------------------------------- /example/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %} 5 | {% trans "Solid urls about" %} 6 | {% endblock title %} 7 | 8 | {% block content %} 9 | {% trans "Information" %} 10 | {% endblock content %} 11 | -------------------------------------------------------------------------------- /example/templates/onelang.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | One language page 5 | {% endblock title %} 6 | 7 | {% block lang_choose %}{% endblock %} 8 | 9 | {% block content %} 10 | One language content here. 11 | {% endblock content %} 12 | -------------------------------------------------------------------------------- /solid_i18n/memory.py: -------------------------------------------------------------------------------- 1 | from threading import local 2 | 3 | _language_from_path = local() 4 | 5 | 6 | def set_language_from_path(language): 7 | _language_from_path.value = language 8 | 9 | 10 | def get_language_from_path(): 11 | return getattr(_language_from_path, 'value', None) 12 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /solid_i18n/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | __author__ = 'st4lk' 3 | __version__ = '1.4.2' 4 | 5 | try: 6 | from django import VERSION 7 | except ImportError: 8 | pass 9 | else: 10 | DEPRECATED_DJANGO_VERSIONS = [] 11 | 12 | if VERSION[:2] in DEPRECATED_DJANGO_VERSIONS: 13 | warnings.warn("Support of Django versions %s will be dropped soon" 14 | % DEPRECATED_DJANGO_VERSIONS, PendingDeprecationWarning) 15 | -------------------------------------------------------------------------------- /example/example/settings_test.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | from django import VERSION 3 | 4 | if VERSION < (1, 8): 5 | INSTALLED_APPS += ( 6 | 'django_nose', 7 | ) 8 | 9 | TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 10 | 11 | 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.sqlite3', 15 | 'NAME': ':memory:', 16 | 'USER': '', 17 | 'PASSWORD': '', 18 | 'TEST_CHARSET': 'utf8', 19 | }} 20 | -------------------------------------------------------------------------------- /example/tests/urls_noni18n.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include 2 | from django.views.generic import TemplateView 3 | 4 | urlpatterns = [ 5 | url(r'^$', TemplateView.as_view(template_name="home.html"), name='home'), 6 | url(r'^about/$', TemplateView.as_view(template_name="about.html"), 7 | name='about'), 8 | ] 9 | 10 | urlpatterns += [ 11 | url(r'^onelang/', TemplateView.as_view(template_name="onelang.html"), 12 | name='onelang'), 13 | url(r'^i18n/', include('django.conf.urls.i18n')), 14 | ] 15 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | py{27,34,35}-django{18,19,110}, 4 | 5 | [testenv] 6 | basepython = 7 | py27: python2.7 8 | py34: python3.4 9 | py35: python3.5 10 | deps = 11 | django18: Django>=1.8,<1.9 12 | django19: Django>=1.9,<1.10 13 | django110: Django>=1.10,<1.11 14 | py27-django110: coverage 15 | -rrequirements_test.txt 16 | setenv = 17 | PYTHONPATH = {toxinidir}/example 18 | LC_ALL = en_US.utf-8 19 | commands = 20 | py.test 21 | 22 | [testenv:py27-django110] 23 | commands = 24 | coverage run --source=solid_i18n -m py.test 25 | coverage report 26 | -------------------------------------------------------------------------------- /example/example/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include 2 | from django.views.generic import TemplateView 3 | 4 | from solid_i18n.urls import solid_i18n_patterns 5 | 6 | urlpatterns = solid_i18n_patterns( 7 | url(r'^$', TemplateView.as_view(template_name="home.html"), name='home'), 8 | url(r'^about/$', TemplateView.as_view(template_name="about.html"), 9 | name='about'), 10 | ) 11 | 12 | # without i18n 13 | urlpatterns += [ 14 | url(r'^onelang/', TemplateView.as_view(template_name="onelang.html"), 15 | name='onelang'), 16 | url(r'^i18n/', include('django.conf.urls.i18n')), 17 | ] 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | 6 | env: 7 | - TOXENV=py34-django18 8 | - TOXENV=py34-django19 9 | - TOXENV=py34-django110 10 | - TOXENV=py35-django18 11 | - TOXENV=py35-django19 12 | - TOXENV=py35-django110 13 | - TOXENV=py27-django18 14 | - TOXENV=py27-django19 15 | - TOXENV=py27-django110 16 | 17 | install: pip install --quiet tox 18 | 19 | # command to run tests 20 | script: tox 21 | 22 | after_script: 23 | - if [ $TOXENV == "py27-django110" ]; then 24 | pip install --quiet coveralls; 25 | coveralls; 26 | fi 27 | 28 | addons: 29 | apt: 30 | sources: 31 | - deadsnakes 32 | packages: 33 | - python3.5 34 | -------------------------------------------------------------------------------- /example/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %} 5 | {% trans "Solid urls home" %} 6 | {% endblock title %} 7 | 8 | {% block content %} 9 |

{% trans "Hello! This is an example home page for django-solid-i18n-urls package usage" %}.

10 |

{% trans "This example site supports two languages: english (default) and russian. As you can see, when accessing url without language prefix, default language is used. It is declared in settings.LANGUAGE_CODE. For more details visit" %} https://github.com/st4lk/django-solid-i18n-urls.

11 |

{% trans "Languages can be switched using buttons in right-up corner" %}.

12 | {% endblock content %} 13 | -------------------------------------------------------------------------------- /solid_i18n/contrib.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains some code, copied from django, to support different versions of django. 3 | """ 4 | from django.utils.encoding import iri_to_uri 5 | from django.utils.encoding import escape_uri_path 6 | 7 | 8 | def get_full_path(request, force_append_slash=False): 9 | """ 10 | Copied from django.http.request.get_full_path (django 1.9) to support 11 | older versions of django. 12 | """ 13 | # RFC 3986 requires query string arguments to be in the ASCII range. 14 | # Rather than crash if this doesn't happen, we encode defensively. 15 | return '%s%s%s' % ( 16 | escape_uri_path(request.path), 17 | '/' if force_append_slash and not request.path.endswith('/') else '', 18 | ('?' + iri_to_uri(request.META.get('QUERY_STRING', ''))) if request.META.get('QUERY_STRING', '') else '' 19 | ) 20 | -------------------------------------------------------------------------------- /example/tests/test_noni18n_urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.core.urlresolvers import reverse 3 | from django.test.utils import override_settings 4 | from django.core.urlresolvers import clear_url_caches 5 | 6 | from .base import URLTestCaseBase 7 | 8 | 9 | class Noni18nUrlsTestCase(URLTestCaseBase): 10 | urls = 'tests.urls_noni18n' 11 | 12 | def test_noni18n_page(self): 13 | url = reverse('onelang') 14 | self.assertEqual(url, '/onelang/') 15 | response = self.client.get(url) 16 | self.assertContains(response, 'One language content') 17 | 18 | 19 | class SettingsChangeTestCase(URLTestCaseBase): 20 | 21 | def setUp(self): 22 | super(URLTestCaseBase, self).tearDown() 23 | clear_url_caches() 24 | # don't reload urlconf here 25 | 26 | @override_settings(USE_I18N=False) 27 | def test_usei18n_false(self): 28 | response = self.client.get(reverse('home')) 29 | self.assertContains(response, 'Hello!') 30 | response = self.client.get(reverse('about')) 31 | self.assertContains(response, 'Information') 32 | response = self.client.get(reverse('onelang')) 33 | self.assertContains(response, 'One language content') 34 | -------------------------------------------------------------------------------- /example/example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, st4lk 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 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution.. 11 | 3. Neither the name of the st4lk nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /solid_i18n/urls.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from django import VERSION as DJANGO_VERSION 3 | from django.conf import settings 4 | from django.utils import lru_cache, six 5 | from django.core.urlresolvers import get_resolver 6 | 7 | from .urlresolvers import SolidLocaleRegexURLResolver 8 | 9 | 10 | def solid_i18n_patterns(prefix, *args): 11 | """ 12 | Modified copy of django i18n_patterns. 13 | Adds the language code prefix to every *non default language* URL pattern 14 | within this function. This may only be used in the root URLconf, 15 | not in an included URLconf. 16 | Do not adds any language code prefix to default language URL pattern. 17 | Default language must be set in settings.LANGUAGE_CODE 18 | """ 19 | if DJANGO_VERSION < (1, 10) and isinstance(prefix, six.string_types): 20 | from django.conf.urls import patterns 21 | warnings.warn( 22 | "Calling solid_i18n_patterns() with the `prefix` argument and with " 23 | "tuples instead of django.conf.urls.url() instances is deprecated and " 24 | "will no longer work in Django 2.0. Use a list of " 25 | "django.conf.urls.url() instances instead.", 26 | PendingDeprecationWarning, stacklevel=2 27 | ) 28 | pattern_list = patterns(prefix, *args) 29 | else: 30 | pattern_list = [prefix] + list(args) 31 | 32 | if not settings.USE_I18N: 33 | return pattern_list 34 | return [SolidLocaleRegexURLResolver(pattern_list)] 35 | 36 | 37 | @lru_cache.lru_cache(maxsize=None) 38 | def is_language_prefix_patterns_used(urlconf): 39 | """ 40 | Returns `True` if the `SolidLocaleRegexURLResolver` is used 41 | at root level of the urlpatterns, else it returns `False`. 42 | """ 43 | for url_pattern in get_resolver(urlconf).url_patterns: 44 | if isinstance(url_pattern, SolidLocaleRegexURLResolver): 45 | return True 46 | return False 47 | -------------------------------------------------------------------------------- /example/tests/base.py: -------------------------------------------------------------------------------- 1 | import sys 2 | try: 3 | from importlib import import_module 4 | except ImportError: 5 | from django.utils.importlib import import_module 6 | try: 7 | from importlib import reload # builtin reload deprecated since version 3.4 8 | except ImportError: 9 | try: 10 | from imp import reload 11 | except ImportError: 12 | pass 13 | from django.conf import settings 14 | from django.test import TestCase 15 | from django.core.urlresolvers import clear_url_caches 16 | from django.utils import translation 17 | try: 18 | from django.test.utils import TransRealMixin 19 | except ImportError: 20 | class TransRealMixin(object): 21 | pass 22 | 23 | from solid_i18n.urls import is_language_prefix_patterns_used 24 | 25 | 26 | def reload_urlconf(urlconf=None, urls_attr='urlpatterns'): 27 | # http://codeinthehole.com/writing/how-to-reload-djangos-url-config/ 28 | if settings.ROOT_URLCONF in sys.modules: 29 | reload(sys.modules[settings.ROOT_URLCONF]) 30 | return import_module(settings.ROOT_URLCONF) 31 | 32 | 33 | class URLTestCaseBase(TransRealMixin, TestCase): 34 | 35 | def setUp(self): 36 | # Make sure the cache is empty before we are doing our tests. 37 | super(URLTestCaseBase, self).tearDown() 38 | clear_url_caches() 39 | is_language_prefix_patterns_used.cache_clear() 40 | reload_urlconf() 41 | 42 | def tearDown(self): 43 | # Make sure we will leave an empty cache for other testcases. 44 | clear_url_caches() 45 | # Not sure why exactly, but TransRealMixin was removied in django 1.7 46 | # look https://github.com/django/django/commit/b87bc461c89f2006f0b27c7240fb488fac32bed1 47 | # Without it, tests will fail with django 1.7, because language 48 | # will be kept in _active.value between tests. 49 | # have to delete _active.value explicitly by calling deactivate 50 | # TODO: investigate this problem more deeply 51 | translation.deactivate() 52 | super(URLTestCaseBase, self).tearDown() 53 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from setuptools import setup, find_packages 4 | from solid_i18n import __author__, __version__ 5 | 6 | 7 | def __read(fname): 8 | try: 9 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 10 | except IOError: 11 | return '' 12 | 13 | if sys.argv[-1] == 'publish': 14 | os.system('pandoc --from=markdown --to=rst --output=README.rst README.md') 15 | os.system('pandoc --from=markdown --to=rst --output=RELEASE_NOTES.rst RELEASE_NOTES.md') 16 | os.system('python setup.py sdist upload') 17 | os.system('python setup.py bdist_wheel upload') 18 | sys.exit() 19 | 20 | if sys.argv[-1] == 'generate_rst': 21 | os.system('pandoc --from=markdown --to=rst --output=README.rst README.md') 22 | os.system('pandoc --from=markdown --to=rst --output=RELEASE_NOTES.rst RELEASE_NOTES.md') 23 | sys.exit() 24 | 25 | if sys.argv[-1] == 'tag': 26 | print("Tagging the version on github:") 27 | os.system("git tag -a v%s -m 'version %s'" % (__version__, __version__)) 28 | os.system("git push --tags") 29 | sys.exit() 30 | 31 | install_requires = __read('requirements.txt').split() 32 | 33 | setup( 34 | name='solid_i18n', 35 | author=__author__, 36 | author_email='alexevseev@gmail.com', 37 | version=__version__, 38 | description='Use default language for urls without language prefix (django)', 39 | long_description=__read('README.rst') + '\n\n' + __read('RELEASE_NOTES.rst'), 40 | platforms=('Any'), 41 | packages=find_packages(), 42 | install_requires=install_requires, 43 | keywords='django i18n urls solid redirects language default'.split(), 44 | include_package_data=True, 45 | license='BSD License', 46 | package_dir={'solid_i18n': 'solid_i18n'}, 47 | url='https://github.com/st4lk/django-solid-i18n-urls', 48 | classifiers=[ 49 | 'Environment :: Web Environment', 50 | 'Framework :: Django', 51 | 'Intended Audience :: Developers', 52 | 'License :: OSI Approved :: BSD License', 53 | 'Operating System :: OS Independent', 54 | 'Programming Language :: Python', 55 | 'Topic :: Utilities', 56 | ], 57 | ) 58 | -------------------------------------------------------------------------------- /example/locale/ru/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: 2013-07-06 02:07+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 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 20 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" 21 | 22 | #: .\templates\about.html.py:5 23 | msgid "Solid urls about" 24 | msgstr "Информация solid urls" 25 | 26 | #: .\templates\about.html.py:9 27 | msgid "Information" 28 | msgstr "Информация" 29 | 30 | #: .\templates\base.html.py:29 31 | msgid "Django solid i18n urls example" 32 | msgstr "Пример django solid i18n urls" 33 | 34 | #: .\templates\base.html.py:32 35 | msgid "Home" 36 | msgstr "Домой" 37 | 38 | #: .\templates\base.html.py:33 39 | msgid "About" 40 | msgstr "О сайте" 41 | 42 | #: .\templates\base.html.py:34 43 | msgid "Link without i18n" 44 | msgstr "Ссылка без перевода" 45 | 46 | #: .\templates\home.html.py:6 47 | msgid "Solid urls home" 48 | msgstr "Домашняя страница Solid urls" 49 | 50 | #: .\templates\home.html.py:10 51 | msgid "" 52 | "Hello! This is an example home page for django-solid-i18n-urls package usage" 53 | msgstr "" 54 | "Здравствуйте! Это пример домашней страницы для использования пакета django-" 55 | "solid-i18n-urls" 56 | 57 | #: .\templates\home.html.py:11 58 | msgid "" 59 | "This example site supports two languages: english (default) and russian. As " 60 | "you can see, when accessing url without language prefix, default language is " 61 | "used. It is declared in settings.LANGUAGE_CODE. For more details visit" 62 | msgstr "" 63 | "Этот тестовый сайт поддерживает два языка: английский (по умолчанию) и " 64 | "русский. Как вы можете видеть, проходя по ссылке без языкового префикса, " 65 | "используется язык по умолчанию. Он объявлен в settings.LANGUAGE_CODE. Для " 66 | "большей информации пройдите по ссылке" 67 | 68 | #: .\templates\home.html.py:12 69 | msgid "Languages can be switched using buttons in right-up corner" 70 | msgstr "Язык можно переключить используя кнопки в правом верхнем углу" 71 | 72 | #~ msgid "One language link" 73 | #~ msgstr "Ссылка без перевода" 74 | 75 | #~ msgid "Not translatable link" 76 | #~ msgstr "Ссылка без перевода" 77 | 78 | #~ msgid "Package url" 79 | #~ msgstr "Страница пакета" 80 | 81 | #~ msgid "You can visit about page here" 82 | #~ msgstr "Вы можете посетить страницу about здесь" 83 | 84 | #~ msgid "About page link" 85 | #~ msgstr "Ссылка на страницу about" 86 | -------------------------------------------------------------------------------- /solid_i18n/urlresolvers.py: -------------------------------------------------------------------------------- 1 | import re 2 | from django.utils.translation import get_language 3 | from django.core.urlresolvers import LocaleRegexURLResolver, clear_url_caches 4 | from django.conf import settings 5 | from .memory import get_language_from_path 6 | 7 | 8 | class SolidLocaleRegexURLResolver(LocaleRegexURLResolver): 9 | """ 10 | A URL resolver that always matches the active language code as URL prefix, 11 | but for default language non prefix is used. 12 | 13 | Rather than taking a regex argument, we just override the ``regex`` 14 | function to always return the active language-code as regex. 15 | """ 16 | def __init__(self, urlconf_name, *args, **kwargs): 17 | super(SolidLocaleRegexURLResolver, self).__init__( 18 | urlconf_name, *args, **kwargs) 19 | self.compiled_with_default = False 20 | 21 | @property 22 | def regex(self): 23 | """ 24 | For non-default language always returns regex with langauge prefix. 25 | For default language returns either '', eigher '^{lang_code}/', 26 | depending on SOLID_I18N_HANDLE_DEFAULT_PREFIX and request url. 27 | 28 | If SOLID_I18N_HANDLE_DEFAULT_PREFIX == True and default langauge 29 | prefix is present in url, all other urls will be reversed with default 30 | prefix. 31 | Otherwise, all other urls will be reversed without default langauge 32 | prefix. 33 | """ 34 | language_code = get_language() 35 | handle_default_prefix = getattr(settings, 'SOLID_I18N_HANDLE_DEFAULT_PREFIX', False) 36 | if language_code not in self._regex_dict: 37 | if language_code != settings.LANGUAGE_CODE: 38 | regex = '^%s/' % language_code 39 | elif handle_default_prefix: 40 | if get_language_from_path() == settings.LANGUAGE_CODE: 41 | self.compiled_with_default = True 42 | regex = '^%s/' % language_code 43 | else: 44 | self.compiled_with_default = False 45 | regex = '' 46 | else: 47 | regex = '' 48 | self._regex_dict[language_code] = re.compile(regex, re.UNICODE) 49 | elif handle_default_prefix and language_code == settings.LANGUAGE_CODE: 50 | language_from_path = get_language_from_path() 51 | regex = None 52 | if self.compiled_with_default and not language_from_path: 53 | # default language is compiled with prefix, but now client 54 | # requests the url without prefix. So compile other urls 55 | # without prefix. 56 | regex = '' 57 | self.compiled_with_default = False 58 | elif not self.compiled_with_default and language_from_path == settings.LANGUAGE_CODE: 59 | # default language is compiled without prefix, but now client 60 | # requests the url with prefix. So compile other urls 61 | # with prefix. 62 | regex = '^%s/' % language_code 63 | self.compiled_with_default = True 64 | if regex is not None: 65 | clear_url_caches() 66 | self._regex_dict[language_code] = re.compile(regex, re.UNICODE) 67 | return self._regex_dict[language_code] 68 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | solid_i18n release notes 2 | ======================== 3 | 4 | v1.4.2 5 | ------ 6 | - Remove requirement for Django < 1.11 in order to use package on Django 1.11. 7 | 8 | Issues: [#43](https://github.com/st4lk/django-solid-i18n-urls/issues/43) 9 | 10 | 11 | v1.4.1 12 | ------ 13 | - Fix minor issue with SolidLocaleRegexURLResolver 14 | 15 | Issues: [#40](https://github.com/st4lk/django-solid-i18n-urls/issues/40) 16 | 17 | 18 | v1.4.0 19 | ------ 20 | - Add django 1.10 support 21 | - Add deprecation notice 22 | 23 | Issues: [#35](https://github.com/st4lk/django-solid-i18n-urls/issues/35) 24 | 25 | v1.3.0 26 | ------ 27 | - Add SOLID_I18N_PREFIX_STRICT setting to handle urls starting with language code 28 | 29 | Issues: [#34](https://github.com/st4lk/django-solid-i18n-urls/issues/34) 30 | 31 | v1.2.0 32 | ------ 33 | - Add django 1.9 support 34 | - Drop django 1.4 support 35 | - Drop python 3.2 support 36 | - Simplify tox settings 37 | 38 | Issues: [#32](https://github.com/st4lk/django-solid-i18n-urls/issues/32), [#23](https://github.com/st4lk/django-solid-i18n-urls/issues/23 ), [#21](https://github.com/st4lk/django-solid-i18n-urls/issues/21) 39 | 40 | v1.1.1 41 | ------ 42 | - fix django 1.8 `AppRegistryNotReady("Apps aren't loaded yet.")` 43 | 44 | Issues: [#29](https://github.com/st4lk/django-solid-i18n-urls/issues/29) 45 | 46 | v1.1.0 47 | ------ 48 | 49 | - Use 301 redirect in case of `SOLID_I18N_DEFAULT_PREFIX_REDIRECT` 50 | - Upload wheel 51 | 52 | Issues: [#24](https://github.com/st4lk/django-solid-i18n-urls/issues/24), [#20](https://github.com/st4lk/django-solid-i18n-urls/issues/20) 53 | 54 | v1.0.0 55 | ------ 56 | 57 | - Add django 1.8 support 58 | 59 | Issues: [#8](https://github.com/st4lk/django-solid-i18n-urls/issues/8), [#19](https://github.com/st4lk/django-solid-i18n-urls/issues/19) 60 | 61 | v0.9.1 62 | ------ 63 | 64 | - fix working with [set_language](https://docs.djangoproject.com/en/dev/topics/i18n/translation/#set-language-redirect-view) and `SOLID_I18N_HANDLE_DEFAULT_PREFIX = True` 65 | 66 | Issues: [#17](https://github.com/st4lk/django-solid-i18n-urls/issues/17) 67 | 68 | v0.8.1 69 | ------ 70 | 71 | - fix url reverse in case of `SOLID_I18N_HANDLE_DEFAULT_PREFIX = True` 72 | - simplify django version checking 73 | 74 | Issues: [#13](https://github.com/st4lk/django-solid-i18n-urls/issues/13), [#14](https://github.com/st4lk/django-solid-i18n-urls/issues/14) 75 | 76 | v0.7.1 77 | ------ 78 | 79 | - add settings `SOLID_I18N_HANDLE_DEFAULT_PREFIX` and `SOLID_I18N_DEFAULT_PREFIX_REDIRECT` 80 | 81 | Issues: [#12](https://github.com/st4lk/django-solid-i18n-urls/issues/12) 82 | 83 | v0.6.1 84 | ------ 85 | 86 | - handle urls with default language prefix explicitly set 87 | 88 | Issues: [#10](https://github.com/st4lk/django-solid-i18n-urls/issues/10) 89 | 90 | v0.5.1 91 | ------ 92 | 93 | - add django 1.7 support 94 | - add python 3.4 support 95 | 96 | Issues: [#6](https://github.com/st4lk/django-solid-i18n-urls/issues/6) 97 | 98 | v0.4.3 99 | ------ 100 | 101 | - fix http header 'Vary Accept-Language' 102 | 103 | Issues: [#4](https://github.com/st4lk/django-solid-i18n-urls/issues/4) 104 | 105 | v0.4.2 106 | ------ 107 | 108 | - stop downgrading Django from 1.6.x to 1.6 109 | - include requirements.txt in distribution 110 | - minor docs updates 111 | 112 | Issues: [#3](https://github.com/st4lk/django-solid-i18n-urls/issues/3) 113 | 114 | v0.4.1 115 | ------ 116 | Add python 3.2, 3.3 support. 117 | 118 | Issues: [#2](https://github.com/st4lk/django-solid-i18n-urls/issues/2) 119 | 120 | v0.3.1 121 | ------ 122 | 123 | Add django 1.6 support 124 | 125 | v0.2.1 126 | ------ 127 | 128 | Update README and data for pypi 129 | 130 | v0.2 131 | ---- 132 | 133 | First version in pypi 134 | -------------------------------------------------------------------------------- /example/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | {% get_current_language as LANGUAGE_CODE %} 5 | 6 | 7 | 8 | {% block title %}{% endblock title %} 9 | 10 | 11 | 12 | 32 | 33 | 34 | 35 | 36 | 100 | 101 |
102 | {% block content %}{% endblock content %} 103 | 104 |
105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /solid_i18n/middleware.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django import VERSION as DJANGO_VERSION 4 | from django.conf import settings 5 | from django.core.urlresolvers import is_valid_path, get_script_prefix 6 | from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect 7 | from django.middleware.locale import LocaleMiddleware 8 | from django.utils import translation as trans 9 | from django.utils.cache import patch_vary_headers 10 | from django.utils.translation.trans_real import language_code_prefix_re 11 | 12 | from .contrib import get_full_path 13 | from .memory import set_language_from_path 14 | from .urls import is_language_prefix_patterns_used 15 | 16 | strict_language_code_prefix_re = re.compile( 17 | r'^/({0})(/|$)'.format( 18 | '|'.join( 19 | map( 20 | re.escape, 21 | dict(settings.LANGUAGES).keys() 22 | ) 23 | ) 24 | ), 25 | flags=re.IGNORECASE 26 | ) 27 | 28 | 29 | def get_language_from_path(path): 30 | """ 31 | django.utils.translation wrapper does't allow/pass strict argument 32 | """ 33 | if settings.USE_I18N: 34 | strict = getattr(settings, 'SOLID_I18N_PREFIX_STRICT', False) 35 | if strict and not strict_language_code_prefix_re.match(path): 36 | return None 37 | # strict below could possibly be removed since the above is in place 38 | return trans.trans_real.get_language_from_path(path, strict=strict) 39 | 40 | 41 | class SolidLocaleMiddleware(LocaleMiddleware): 42 | """ 43 | Request without language prefix will use default language. 44 | Or, if settings.SOLID_I18N_USE_REDIRECTS is True, try to discover language. 45 | If language is not equal to default language, redirect to discovered 46 | language. 47 | 48 | If request contains language prefix, this language will be used immediately. 49 | In that case settings.SOLID_I18N_USE_REDIRECTS doesn't make sense. 50 | 51 | Default language is set in settings.LANGUAGE_CODE. 52 | """ 53 | response_redirect_class = HttpResponseRedirect 54 | response_default_language_redirect_class = HttpResponsePermanentRedirect 55 | 56 | @property 57 | def use_redirects(self): 58 | return getattr(settings, 'SOLID_I18N_USE_REDIRECTS', False) 59 | 60 | @property 61 | def default_lang(self): 62 | return settings.LANGUAGE_CODE 63 | 64 | def process_request(self, request): 65 | urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF) 66 | check_path = is_language_prefix_patterns_used(urlconf) 67 | language_path = get_language_from_path(request.path_info) 68 | if check_path and not self.use_redirects: 69 | language = language_path or self.default_lang 70 | else: 71 | language = trans.get_language_from_request(request, check_path) 72 | set_language_from_path(language_path) 73 | trans.activate(language) 74 | request.LANGUAGE_CODE = trans.get_language() 75 | 76 | def process_response(self, request, response): 77 | language = trans.get_language() 78 | language_from_path = get_language_from_path(request.path_info) 79 | urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF) 80 | i18n_patterns_used = is_language_prefix_patterns_used(urlconf) 81 | if (getattr(settings, 'SOLID_I18N_DEFAULT_PREFIX_REDIRECT', False) and 82 | language_from_path == self.default_lang and 83 | i18n_patterns_used): 84 | redirect = self.perform_redirect(request, '', is_permanent=True) 85 | if redirect: 86 | return redirect 87 | elif self.use_redirects: 88 | if (response.status_code == 404 and not language_from_path and 89 | i18n_patterns_used and 90 | language != self.default_lang): 91 | redirect = self.perform_redirect(request, language) 92 | if redirect: 93 | return redirect 94 | if not (i18n_patterns_used and language_from_path): 95 | patch_vary_headers(response, ('Accept-Language',)) 96 | if 'Content-Language' not in response: 97 | response['Content-Language'] = language 98 | return response 99 | 100 | def remove_lang_from_path(self, path): 101 | no_lang_tag_path = path 102 | regex_match = language_code_prefix_re.match(path) 103 | if regex_match: 104 | lang_code = regex_match.group(1) 105 | no_lang_tag_path = path[1 + len(lang_code):] 106 | if not no_lang_tag_path.startswith('/'): 107 | no_lang_tag_path = '/' + no_lang_tag_path 108 | return no_lang_tag_path 109 | 110 | def perform_redirect(self, request, language, is_permanent=False): 111 | # language can be empty string (in case of default language) 112 | path_info = request.path_info 113 | if not language: 114 | path_info = self.remove_lang_from_path(path_info) 115 | urlconf = getattr(request, 'urlconf', None) 116 | language_path = '%s%s' % (language, path_info) 117 | if not language_path.startswith('/'): 118 | language_path = '/' + language_path 119 | path_valid = is_valid_path(language_path, urlconf) 120 | path_needs_slash = ( 121 | not path_valid and ( 122 | settings.APPEND_SLASH and not language_path.endswith('/') and 123 | is_valid_path('%s/' % language_path, urlconf) 124 | ) 125 | ) 126 | 127 | if path_valid or path_needs_slash: 128 | script_prefix = get_script_prefix() 129 | if DJANGO_VERSION < (1, 9): 130 | full_path = get_full_path(request, force_append_slash=path_needs_slash) 131 | else: 132 | full_path = request.get_full_path(force_append_slash=path_needs_slash) 133 | if not language: 134 | full_path = self.remove_lang_from_path(full_path) 135 | language_url = full_path.replace( 136 | script_prefix, 137 | '%s%s/' % (script_prefix, language) if language else script_prefix, 138 | 1 139 | ) 140 | 141 | # return a 301 permanent redirect if on default language 142 | if (is_permanent): 143 | return self.response_default_language_redirect_class(language_url) 144 | else: 145 | return self.response_redirect_class(language_url) 146 | -------------------------------------------------------------------------------- /example/example/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for example project. 2 | import os 3 | 4 | location = lambda x: os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', x) 5 | 6 | DEBUG = True 7 | 8 | ADMINS = ( 9 | # ('Your Name', 'your_email@example.com'), 10 | ) 11 | 12 | MANAGERS = ADMINS 13 | 14 | DATABASES = { 15 | 'default': { 16 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 17 | 'NAME': 'db.sqlite', # Or path to database file if using sqlite3. 18 | 'USER': '', # Not used with sqlite3. 19 | 'PASSWORD': '', # Not used with sqlite3. 20 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 21 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 22 | } 23 | } 24 | 25 | # Hosts/domain names that are valid for this site; required if DEBUG is False 26 | # See https://docs.djangoproject.com/en/1.4/ref/settings/#allowed-hosts 27 | ALLOWED_HOSTS = [] 28 | 29 | # Local time zone for this installation. Choices can be found here: 30 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 31 | # although not all choices may be available on all operating systems. 32 | # In a Windows environment this must be set to your system time zone. 33 | TIME_ZONE = 'America/Chicago' 34 | 35 | # Language code for this installation. All choices can be found here: 36 | # http://www.i18nguy.com/unicode/language-identifiers.html 37 | LANGUAGE_CODE = 'en' 38 | 39 | # supported languages 40 | LANGUAGES = ( 41 | ('ru', 'Russian'), 42 | ('en', 'English'), 43 | ('my', 'Burmese'), 44 | ('pt-br', 'Brazilian Portuguese'), 45 | ) 46 | 47 | LOCALE_PATHS = ( 48 | location('locale'), 49 | ) 50 | 51 | SITE_ID = 1 52 | 53 | # If you set this to False, Django will make some optimizations so as not 54 | # to load the internationalization machinery. 55 | USE_I18N = True 56 | 57 | # If you set this to False, Django will not format dates, numbers and 58 | # calendars according to the current locale. 59 | USE_L10N = True 60 | 61 | # If you set this to False, Django will not use timezone-aware datetimes. 62 | USE_TZ = True 63 | 64 | # Absolute filesystem path to the directory that will hold user-uploaded files. 65 | # Example: "/home/media/media.lawrence.com/media/" 66 | MEDIA_ROOT = '' 67 | 68 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 69 | # trailing slash. 70 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 71 | MEDIA_URL = '' 72 | 73 | # Absolute path to the directory static files should be collected to. 74 | # Don't put anything in this directory yourself; store your static files 75 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 76 | # Example: "/home/media/media.lawrence.com/static/" 77 | STATIC_ROOT = '' 78 | 79 | # URL prefix for static files. 80 | # Example: "http://media.lawrence.com/static/" 81 | STATIC_URL = '/static/' 82 | 83 | # Additional locations of static files 84 | STATICFILES_DIRS = ( 85 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 86 | # Always use forward slashes, even on Windows. 87 | # Don't forget to use absolute paths, not relative paths. 88 | ) 89 | 90 | # List of finder classes that know how to find static files in 91 | # various locations. 92 | STATICFILES_FINDERS = ( 93 | 'django.contrib.staticfiles.finders.FileSystemFinder', 94 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 95 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 96 | ) 97 | 98 | # Make this unique, and don't share it with anybody. 99 | SECRET_KEY = 'x%u66q%jc7$&cy3%w2knw84tqc8i@e^9ag_f19w6ace@5a8jj0' 100 | 101 | MIDDLEWARE_CLASSES = ( 102 | 'django.middleware.common.CommonMiddleware', 103 | 'django.contrib.sessions.middleware.SessionMiddleware', 104 | 'django.middleware.csrf.CsrfViewMiddleware', 105 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 106 | 'django.contrib.messages.middleware.MessageMiddleware', 107 | 'solid_i18n.middleware.SolidLocaleMiddleware', 108 | # Uncomment the next line for simple clickjacking protection: 109 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 110 | ) 111 | 112 | ROOT_URLCONF = 'example.urls' 113 | 114 | # Python dotted path to the WSGI application used by Django's runserver. 115 | WSGI_APPLICATION = 'example.wsgi.application' 116 | 117 | TEMPLATES = [ 118 | { 119 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 120 | 'APP_DIRS': True, 121 | 'DIRS': [ 122 | location('templates'), 123 | ], 124 | 'OPTIONS': { 125 | 'context_processors': [ 126 | 'django.contrib.auth.context_processors.auth', 127 | 'django.template.context_processors.debug', 128 | 'django.template.context_processors.i18n', 129 | 'django.template.context_processors.media', 130 | 'django.template.context_processors.static', 131 | 'django.template.context_processors.tz', 132 | 'django.contrib.messages.context_processors.messages', 133 | 'django.template.context_processors.request', 134 | 'django.template.context_processors.i18n', 135 | 'example.context_processors.solid_i18n', 136 | ], 137 | }, 138 | }, 139 | ] 140 | 141 | INSTALLED_APPS = ( 142 | 'django.contrib.auth', 143 | 'django.contrib.contenttypes', 144 | 'django.contrib.sessions', 145 | 'django.contrib.sites', 146 | 'django.contrib.messages', 147 | 'django.contrib.staticfiles', 148 | # Uncomment the next line to enable the admin: 149 | # 'django.contrib.admin', 150 | # Uncomment the next line to enable admin documentation: 151 | # 'django.contrib.admindocs', 152 | ) 153 | 154 | SOLID_I18N_USE_REDIRECTS = False 155 | SOLID_I18N_HANDLE_DEFAULT_PREFIX = False 156 | SOLID_I18N_DEFAULT_PREFIX_REDIRECT = False 157 | SOLID_I18N_PREFIX_STRICT = False 158 | 159 | # A sample logging configuration. The only tangible logging 160 | # performed by this configuration is to send an email to 161 | # the site admins on every HTTP 500 error when DEBUG=False. 162 | # See http://docs.djangoproject.com/en/dev/topics/logging for 163 | # more details on how to customize your logging configuration. 164 | LOGGING = { 165 | 'version': 1, 166 | 'disable_existing_loggers': False, 167 | 'filters': { 168 | 'require_debug_false': { 169 | '()': 'django.utils.log.RequireDebugFalse' 170 | } 171 | }, 172 | 'handlers': { 173 | 'mail_admins': { 174 | 'level': 'ERROR', 175 | 'filters': ['require_debug_false'], 176 | 'class': 'django.utils.log.AdminEmailHandler' 177 | } 178 | }, 179 | 'loggers': { 180 | 'django.request': { 181 | 'handlers': ['mail_admins'], 182 | 'level': 'ERROR', 183 | 'propagate': True, 184 | }, 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django solid_i18n urls 2 | ===== 3 | 4 | [![Build Status](https://travis-ci.org/st4lk/django-solid-i18n-urls.svg?branch=master)](https://travis-ci.org/st4lk/django-solid-i18n-urls) 5 | [![Coverage Status](https://coveralls.io/repos/st4lk/django-solid-i18n-urls/badge.svg?branch=master)](https://coveralls.io/r/st4lk/django-solid-i18n-urls?branch=master) 6 | [![Pypi version](https://img.shields.io/pypi/v/solid_i18n.svg)](https://pypi.python.org/pypi/solid_i18n) 7 | 8 | solid_i18n contains middleware and url patterns to use default language at root path (without language prefix). 9 | 10 | Default language is set in settings.LANGUAGE_CODE. 11 | 12 | Deprecation notice 13 | ------------------ 14 | Starting from [Django 1.10](https://docs.djangoproject.com/en/dev/releases/1.10/#internationalization), built-in `i18n_patterns` accept optional argument `prefix_default_language`. If it is `False`, then Django will serve url without language prefix by itself. Look [docs](https://docs.djangoproject.com/en/dev/topics/i18n/translation/#django.conf.urls.i18n.i18n_patterns) for more details. 15 | 16 | This package can still be useful in following cases (look below for settings details): 17 | - You need `settings.SOLID_I18N_USE_REDIRECTS = True` behaviour 18 | - You need `settings.SOLID_I18N_HANDLE_DEFAULT_PREFIX = True` behaviour 19 | - You need `settings.SOLID_I18N_DEFAULT_PREFIX_REDIRECT = True` behaviour 20 | - You need `settings.SOLID_I18N_PREFIX_STRICT = True` behaviour 21 | 22 | In all other cases no need in current package, just use Django>=1.10. 23 | 24 | 25 | Requirements 26 | ----------- 27 | 28 | - python (2.7, 3.4, 3.5) 29 | - django (1.8, 1.9, 1.10) 30 | 31 | Release notes 32 | ------------- 33 | 34 | [Here](https://github.com/st4lk/django-solid-i18n-urls/blob/master/RELEASE_NOTES.md) 35 | 36 | 37 | Behaviour 38 | ----------- 39 | 40 | There are two modes: 41 | 42 | 1. `settings.SOLID_I18N_USE_REDIRECTS = False` (default). In that case i18n 43 | will not use redirects at all. If request doesn't have language prefix, 44 | then default language will be used. If request does have prefix, language 45 | from that prefix will be used. 46 | 47 | 2. `settings.SOLID_I18N_USE_REDIRECTS = True`. In that case, for root paths (without 48 | prefix), django will [try to discover](https://docs.djangoproject.com/en/dev/topics/i18n/translation/#how-django-discovers-language-preference) user preferred language. If it doesn't equal to default language, redirect to path with corresponding 49 | prefix will occur. If preferred language is the same as default, then that request 50 | path will be processed (without redirect). Also see notes below. 51 | 52 | 53 | Quick start 54 | ----------- 55 | 56 | 1. Install this package to your python distribution: 57 | 58 | pip install solid_i18n 59 | 60 | 2. Set languages in settings.py: 61 | 62 | # Default language, that will be used for requests without language prefix 63 | LANGUAGE_CODE = 'en' 64 | 65 | # supported languages 66 | LANGUAGES = ( 67 | ('en', 'English'), 68 | ('ru', 'Russian'), 69 | ) 70 | 71 | # enable django translation 72 | USE_I18N = True 73 | 74 | # Optional. If you want to use redirects, set this to True 75 | SOLID_I18N_USE_REDIRECTS = False 76 | 77 | 3. Add `SolidLocaleMiddleware` instead of [LocaleMiddleware](https://docs.djangoproject.com/en/dev/ref/middleware/#django.middleware.locale.LocaleMiddleware) to `MIDDLEWARE_CLASSES`: 78 | 79 | MIDDLEWARE_CLASSES = ( 80 | 'django.contrib.sessions.middleware.SessionMiddleware', 81 | 'solid_i18n.middleware.SolidLocaleMiddleware', 82 | 'django.middleware.common.CommonMiddleware', 83 | ) 84 | 85 | 4. Use `solid_i18n_patterns` instead of [i18n_patterns](https://docs.djangoproject.com/en/dev/topics/i18n/translation/#django.conf.urls.i18n.i18n_patterns) 86 | 87 | from django.conf.urls import patterns, include, url 88 | from solid_i18n.urls import solid_i18n_patterns 89 | 90 | urlpatterns = solid_i18n_patterns( 91 | url(r'^about/$', 'about.view', name='about'), 92 | url(r'^news/', include(news_patterns, namespace='news')), 93 | ) 94 | 95 | 5. Start the development server and visit http://127.0.0.1:8000/about/ to see english content. Visit http://127.0.0.1:8000/ru/about/ to see russian content. If `SOLID_I18N_USE_REDIRECTS` was set to `True` and if your preferred language is equal to Russian, request to path http://127.0.0.1:8000/about/ will be redirected to http://127.0.0.1:8000/ru/about/. But if preferred language is English, http://127.0.0.1:8000/about/ will be shown. 96 | 97 | Settings 98 | -------- 99 | 100 | - `SOLID_I18N_USE_REDIRECTS = False` 101 | If `True`, redirect to url with non-default language prefix from url without prefix, if user's language is not equal to default. Otherwise url without language prefix will always render default language content (see [behaviour section](#behaviour) and [notes](#notes) for details). 102 | 103 | - `SOLID_I18N_HANDLE_DEFAULT_PREFIX = False` 104 | If `True`, both urls `/...` and `/en/...` will render default language content (in this example 'en' is default language). 105 | Otherwise, `/en/...` will return 404 status_code. 106 | 107 | - `SOLID_I18N_DEFAULT_PREFIX_REDIRECT = False` 108 | If `True`, redirect from url with default language prefix to url without any prefix, i.e. redirect from `/en/...` to `/...` if 'en' is default language. 109 | 110 | - `SOLID_I18N_PREFIX_STRICT = False` 111 | Experimental. If `True`, paths like `/my-slug/` will call your view on that path, if language my-slug doesn't exists (here `my` is supported language). 112 | 113 | Example. 114 | 115 | # settings.py 116 | LANGUAGES = ( 117 | ('en', 'English'), 118 | ('my', 'Burmese'), 119 | ) 120 | 121 | # urls.py 122 | urlpatterns = solid_i18n_patterns('', 123 | url(r'^my-slug/$', some_view), 124 | ) 125 | 126 | If `SOLID_I18N_PREFIX_STRICT=False`, then url /my-slug/ will respond with 404, since language `my-slug` is not found. 127 | This happens, because we have a registered language tag `my`. Language tag can have form like this: 128 | 129 | language-region 130 | 131 | So django in this case tries to find language 'my-slug'. But it fails and that is why django respond 404. 132 | And your view `some_view` will not be called. 133 | 134 | But, if we set `SOLID_I18N_PREFIX_STRICT=True`, then resolve system will get language only from exact 'my' prefix. 135 | In case of /my-slug/ url the prefix is not exact, and our `some_view` will be found and called. 136 | 137 | Example site 138 | ----------- 139 | 140 | Located [here](https://github.com/st4lk/django-solid-i18n-urls/tree/master/example), it is ready to use, just install solid_i18n (this package): 141 | 142 | pip install solid_i18n 143 | 144 | clone example site: 145 | 146 | git clone https://github.com/st4lk/django-solid-i18n-urls.git 147 | 148 | step in example/ and run development server: 149 | 150 | cd django-solid-i18n-urls/example 151 | python manage.py runserver 152 | 153 | 154 | Notes 155 | ----------- 156 | 157 | - When using `SOLID_I18N_USE_REDIRECTS = True`, there is some nasty case. Suppose django has determined user preferred language incorrectly (maybe in user's browser preferred language is not equal to his realy preferred language, because for example it is not his computer) and it is Russian. Then on access to url without prefix, i.e. `'/'`, he will be redirected to `'/ru/'` (according to browsers preferred language). He wants to look english content (that is default language), but he can't, because he is always being redirected to `'/ru/'` from `'/'`. To avoid this, it is needed to set preferred language in his cookies (just `` will not work). For that purporse django's [set_language redirect view](https://docs.djangoproject.com/en/dev/topics/i18n/translation/#the-set-language-redirect-view) shall be used. See example in this package. 158 | 159 | - Of course, you must specify translation for all languages you've marked as supported. For details look here: [https://docs.djangoproject.com/en/dev/topics/i18n/translation/](https://docs.djangoproject.com/en/dev/topics/i18n/translation/). 160 | 161 | - Don't mix together settings `SOLID_I18N_HANDLE_DEFAULT_PREFIX` and `SOLID_I18N_DEFAULT_PREFIX_REDIRECT`. You should choose only one of them. 162 | -------------------------------------------------------------------------------- /example/tests/test_solid_urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.core.urlresolvers import reverse 5 | from django.conf.urls import url 6 | from django.utils import translation 7 | from django.test.utils import override_settings 8 | from django.views.generic import TemplateView 9 | from solid_i18n.urls import solid_i18n_patterns 10 | 11 | from .base import URLTestCaseBase 12 | 13 | 14 | class PrefixDeprecationTestCase(URLTestCaseBase): 15 | 16 | def setUp(self): 17 | super(PrefixDeprecationTestCase, self).setUp() 18 | self.test_urls = [ 19 | url(r'^$', TemplateView.as_view(template_name="test.html"), name='test'), 20 | url(r'^$', TemplateView.as_view(template_name="test2.html"), name='test2'), 21 | ] 22 | 23 | def test_with_and_without_prefix(self): 24 | """ 25 | Ensure that solid_i18n_patterns works the same with or without a prefix. 26 | 27 | """ 28 | self.assertEqual( 29 | solid_i18n_patterns(*self.test_urls)[0].regex, 30 | solid_i18n_patterns('', *self.test_urls)[0].regex, 31 | ) 32 | 33 | 34 | class TranslationReverseUrlTestCase(URLTestCaseBase): 35 | 36 | def _base_page_check(self, url_name, url_path): 37 | self.assertEqual(reverse(url_name), url_path) 38 | with translation.override('en'): 39 | self.assertEqual(reverse(url_name), url_path) 40 | with translation.override('ru'): 41 | self.assertEqual(reverse(url_name), '/ru' + url_path) 42 | 43 | # ----------- tests ---------- 44 | 45 | def test_home_page(self): 46 | self._base_page_check('home', '/') 47 | 48 | def test_about_page(self): 49 | self._base_page_check('about', '/about/') 50 | 51 | @override_settings(SOLID_I18N_USE_REDIRECTS=True) 52 | def test_home_page_redirects(self): 53 | self._base_page_check('home', '/') 54 | 55 | @override_settings(SOLID_I18N_USE_REDIRECTS=True) 56 | def test_about_page_redirects(self): 57 | self._base_page_check('about', '/about/') 58 | 59 | 60 | class TranslationAccessTestCase(URLTestCaseBase): 61 | PAGE_DATA = { 62 | "ru": { 63 | "home": 'Здравствуйте!', 64 | "about": 'Информация', 65 | }, 66 | "en": { 67 | "home": 'Hello!', 68 | "about": 'Information', 69 | } 70 | } 71 | 72 | def _check_vary_accept_language(self, response): 73 | from django.conf import settings 74 | vary = response._headers.get('vary', ('', ''))[-1] 75 | if settings.SOLID_I18N_USE_REDIRECTS: 76 | req_path = response.request['PATH_INFO'] 77 | if req_path.startswith('/en') or req_path.startswith('/ru'): 78 | self.assertFalse('Accept-Language' in vary) 79 | else: 80 | self.assertTrue('Accept-Language' in vary) 81 | else: 82 | self.assertFalse('Accept-Language' in vary) 83 | 84 | def _base_page_check(self, response, lang_code, page_code): 85 | self.assertEqual(response.status_code, 200) 86 | content = self.PAGE_DATA[lang_code][page_code] 87 | self.assertTrue(content in response.content.decode('utf8')) 88 | self.assertEqual(response.context['LANGUAGE_CODE'], lang_code) 89 | self._check_vary_accept_language(response) 90 | # content-language 91 | content_lang = response._headers.get('content-language', ('', ''))[-1] 92 | self.assertEqual(content_lang, lang_code) 93 | 94 | @property 95 | def en_http_headers(self): 96 | return dict(HTTP_ACCEPT_LANGUAGE='en-US,en;q=0.8,ru;q=0.6') 97 | 98 | @property 99 | def ru_http_headers(self): 100 | return dict(HTTP_ACCEPT_LANGUAGE='ru-RU,ru;q=0.8,en;q=0.6') 101 | 102 | # ----------- tests ---------- 103 | 104 | def test_home_page_en(self): 105 | with translation.override('en'): 106 | response = self.client.get(reverse('home')) 107 | self._base_page_check(response, "en", "home") 108 | 109 | def test_home_page_ru(self): 110 | with translation.override('ru'): 111 | response = self.client.get(reverse('home')) 112 | self._base_page_check(response, 'ru', "home") 113 | 114 | def test_about_page_en(self): 115 | with translation.override('en'): 116 | response = self.client.get(reverse('about')) 117 | self._base_page_check(response, "en", "about") 118 | 119 | def test_about_page_ru(self): 120 | with translation.override('ru'): 121 | response = self.client.get(reverse('about')) 122 | self._base_page_check(response, "ru", "about") 123 | 124 | def test_home_page_default_prefix_en_404(self): 125 | with translation.override('en'): 126 | response = self.client.get('/en/') 127 | self.assertEqual(response.status_code, 404) 128 | 129 | def test_home_page_default_prefix_ru_404(self): 130 | with translation.override('ru'): 131 | response = self.client.get('/en/') 132 | self.assertEqual(response.status_code, 404) 133 | 134 | # settings 135 | 136 | @override_settings(SOLID_I18N_PREFIX_STRICT=False) 137 | def test_about_page_strict_prefix_false(self): 138 | response = self.client.get('/my-slug/') 139 | self.assertEqual(response._headers.get('content-language')[-1], 'my') 140 | response = self.client.get('/ru/slug/') 141 | self.assertEqual(response._headers.get('content-language')[-1], 'ru') 142 | response = self.client.get('/pt-br/slug/') 143 | self.assertEqual(response._headers.get('content-language')[-1], 'pt-br') 144 | response = self.client.get('/pt-broughton/slug/') 145 | self.assertEqual(response._headers.get('content-language')[-1], 'pt-br') 146 | 147 | @override_settings(SOLID_I18N_PREFIX_STRICT=True) 148 | def test_about_page_strict_prefix_true(self): 149 | response = self.client.get('/my-slug/') 150 | self.assertEqual(response._headers.get('content-language')[-1], 'en') 151 | response = self.client.get('/ru/slug/') 152 | self.assertEqual(response._headers.get('content-language')[-1], 'ru') 153 | response = self.client.get('/pt-br/slug/') 154 | self.assertEqual(response._headers.get('content-language')[-1], 'pt-br') 155 | response = self.client.get('/pt-broughton/slug/') 156 | self.assertEqual(response._headers.get('content-language')[-1], 'en') 157 | 158 | @override_settings(SOLID_I18N_HANDLE_DEFAULT_PREFIX=True) 159 | def test_about_page_default_prefix_en_with_prefix_first(self): 160 | # with prefix 161 | response = self.client.get('/en/about/') 162 | self._base_page_check(response, "en", "about") 163 | self.assertTrue('/en/about/' in str(response.content)) 164 | # without prefix 165 | response = self.client.get('/about/') 166 | self._base_page_check(response, "en", "about") 167 | self.assertTrue('/about/' in str(response.content)) 168 | # again with prefix 169 | response = self.client.get('/en/about/') 170 | self._base_page_check(response, "en", "about") 171 | self.assertTrue('/en/about/' in str(response.content)) 172 | 173 | @override_settings(SOLID_I18N_HANDLE_DEFAULT_PREFIX=True) 174 | def test_about_page_default_prefix_en_without_prefix_first(self): 175 | # without prefix 176 | response = self.client.get('/about/') 177 | self._base_page_check(response, "en", "about") 178 | self.assertTrue('/about/' in str(response.content)) 179 | # with prefix 180 | response = self.client.get('/en/about/') 181 | self._base_page_check(response, "en", "about") 182 | self.assertTrue('/en/about/' in str(response.content)) 183 | # again without prefix 184 | response = self.client.get('/about/') 185 | self._base_page_check(response, "en", "about") 186 | self.assertTrue('/about/' in str(response.content)) 187 | 188 | @override_settings(SOLID_I18N_HANDLE_DEFAULT_PREFIX=True) 189 | def test_about_page_default_prefix_ru(self): 190 | with translation.override('ru'): 191 | response = self.client.get('/en/about/') 192 | self._base_page_check(response, "en", "about") 193 | self.assertTrue('/en/about/' in str(response.content)) 194 | 195 | response = self.client.get('/ru/about/') 196 | self._base_page_check(response, "ru", "about") 197 | self.assertTrue('/ru/about/' in response.content.decode('utf8')) 198 | 199 | @override_settings(SOLID_I18N_HANDLE_DEFAULT_PREFIX=True) 200 | def test_home_page_default_prefix_en(self): 201 | """ 202 | Check, that url with explicit default language prefix is still 203 | accessible. 204 | """ 205 | with translation.override('en'): 206 | response = self.client.get('/en/') 207 | self._base_page_check(response, "en", "home") 208 | 209 | @override_settings(SOLID_I18N_HANDLE_DEFAULT_PREFIX=True) 210 | def test_home_page_default_prefix_ru(self): 211 | """ 212 | Check, that language is got from url prefix, even this laguage is 213 | not equal to client preferred langauge. 214 | """ 215 | with translation.override('ru'): 216 | response = self.client.get('/en/') 217 | self._base_page_check(response, "en", "home") 218 | 219 | @override_settings(SOLID_I18N_DEFAULT_PREFIX_REDIRECT=True) 220 | def test_home_page_default_prefix_en_redirect(self): 221 | with translation.override('en'): 222 | response = self.client.get('/en/') 223 | self.assertEqual(response.status_code, 301) 224 | self.assertTrue('/en/' not in response['Location']) 225 | response = self.client.get(response['Location']) 226 | self._base_page_check(response, "en", "home") 227 | 228 | @override_settings(SOLID_I18N_DEFAULT_PREFIX_REDIRECT=True) 229 | def test_home_page_default_prefix_ru_redirect(self): 230 | with translation.override('ru'): 231 | response = self.client.get('/en/') 232 | self.assertEqual(response.status_code, 301) 233 | self.assertTrue('/en/' not in response['Location']) 234 | response = self.client.get(response['Location']) 235 | self._base_page_check(response, "en", "home") 236 | 237 | # use redirects 238 | 239 | @override_settings(SOLID_I18N_USE_REDIRECTS=True) 240 | def test_home_page_redirects_default_lang(self): 241 | response = self.client.get('/', **self.en_http_headers) 242 | self._base_page_check(response, "en", "home") 243 | 244 | @override_settings(SOLID_I18N_USE_REDIRECTS=True) 245 | def test_home_page_redirects_non_default_lang(self): 246 | response = self.client.get('/', **self.ru_http_headers) 247 | self.assertEqual(response.status_code, 302) 248 | self.assertTrue('/ru/' in response['Location']) 249 | response = self.client.get(response['Location'], **self.ru_http_headers) 250 | self._base_page_check(response, 'ru', "home") 251 | 252 | @override_settings(SOLID_I18N_USE_REDIRECTS=True) 253 | def test_about_page_redirects_default_lang(self): 254 | response = self.client.get('/about/', **self.en_http_headers) 255 | self._base_page_check(response, "en", "about") 256 | 257 | @override_settings(SOLID_I18N_USE_REDIRECTS=True) 258 | def test_about_page_redirects_non_default_lang(self): 259 | response = self.client.get('/about/', **self.ru_http_headers) 260 | self.assertEqual(response.status_code, 302) 261 | self.assertTrue('/ru/about/' in response['Location']) 262 | response = self.client.get(response['Location'], **self.ru_http_headers) 263 | self._base_page_check(response, "ru", "about") 264 | 265 | @override_settings(SOLID_I18N_USE_REDIRECTS=True) 266 | def test_home_page_prefix_default_prefix_en_404(self): 267 | response = self.client.get('/en/', **self.en_http_headers) 268 | self.assertEqual(response.status_code, 404) 269 | 270 | @override_settings(SOLID_I18N_USE_REDIRECTS=True) 271 | def test_home_page_prefix_default_prefix_ru_404(self): 272 | response = self.client.get('/en/', **self.ru_http_headers) 273 | self.assertEqual(response.status_code, 404) 274 | 275 | # settings 276 | 277 | @override_settings(SOLID_I18N_USE_REDIRECTS=True) 278 | @override_settings(SOLID_I18N_HANDLE_DEFAULT_PREFIX=True) 279 | def test_set_language(self): 280 | response = self.client.post('/i18n/setlang/', {'language': 'en', 'next': '/'}) 281 | self.assertEqual(response.status_code, 302) 282 | response = self.client.get(response['Location']) 283 | self.assertEqual(response.status_code, 200) 284 | response = self.client.get('/en/') 285 | self.assertEqual(response.status_code, 200) 286 | response = self.client.get('/ru/') 287 | self.assertEqual(response.status_code, 200) 288 | 289 | @override_settings(SOLID_I18N_USE_REDIRECTS=True) 290 | @override_settings(SOLID_I18N_HANDLE_DEFAULT_PREFIX=True) 291 | def test_home_page_prefix_default_prefix_en(self): 292 | response = self.client.get('/en/', **self.en_http_headers) 293 | self._base_page_check(response, "en", "home") 294 | 295 | @override_settings(SOLID_I18N_USE_REDIRECTS=True) 296 | @override_settings(SOLID_I18N_HANDLE_DEFAULT_PREFIX=True) 297 | def test_home_page_prefix_default_prefix_ru(self): 298 | response = self.client.get('/en/', **self.ru_http_headers) 299 | self._base_page_check(response, "en", "home") 300 | 301 | @override_settings(SOLID_I18N_USE_REDIRECTS=True) 302 | @override_settings(SOLID_I18N_DEFAULT_PREFIX_REDIRECT=True) 303 | def test_home_page_prefix_default_prefix_en_redirect(self): 304 | response = self.client.get('/en/about/', **self.en_http_headers) 305 | self.assertEqual(response.status_code, 301) 306 | self.assertTrue('/about/' in response['Location']) 307 | self.assertFalse('/en/about/' in response['Location']) 308 | self.assertFalse('/ru/about/' in response['Location']) 309 | 310 | @override_settings(SOLID_I18N_USE_REDIRECTS=True) 311 | @override_settings(SOLID_I18N_DEFAULT_PREFIX_REDIRECT=True) 312 | def test_home_page_prefix_default_prefix_ru_redirect(self): 313 | response = self.client.get('/en/about/', **self.ru_http_headers) 314 | self.assertEqual(response.status_code, 301) 315 | self.assertTrue('/about/' in response['Location']) 316 | self.assertFalse('/en/about/' in response['Location']) 317 | self.assertFalse('/ru/about/' in response['Location']) 318 | --------------------------------------------------------------------------------