1. Support translation

Sept. 14, 2017, 2:12 a.m.

The first problem was in Levels, that did not solve enough fastly, is support of some languages of site, and it's need that part of urls built normally for different languages, i. e. with language prefix before url, but other part of urls did not contain this prefixes. For illustrative purposes, I show some urls:

levels.pro/about/ - "About company" in English
levels.pro/ru/about/ - "About company" in Russian
levels.pro/de/about/ - "About company" in German
levels.pro/messages/ - user messages
levels.pro/friends/ - friend list of user

levels.pro/about/, levels.pro/ru/about/, levels.pro/de/about/ urls, as you see, contains information about company in English, in Russian and German language respectively. Language prefix before /about/ tell language in which information will be presented.

But it is needn't language prefix for levels.pro/messages/ and levels.pro/friends/ urls, because this urls of specific authorizes site user. It's better to make similar links identical (without prefixing) regardless of chosen language - this is convenient and beautiful way to represent url.

But for promote the site in search engine, information links such as "About company" should be made as separate pages for each language. As a result, personal user sections (Messages, Friends and so on) are closed from search engines, but information "About company", "Advertising" and so on is open and will be indexed by search engines.

Pay should also pay attention to "Main url", i. e. to levels.pro/. For unauthorized users you should show greeting page with an offer to log or register. But if user logs in, then he should look, say, his personal page or news feed.

Having understood logic of url building, let's move on to implement.

Project structure

For illustrative purposes, I show part of project structure, that there is at the time of writing the article:

levels
├─ profile
│  ├─ templates
│  │  └─ friends.html
│  ├─ __init__.py
│  ├─ urls.py
│  └─ views.py
├─ __init__.py
├─ settings.py
└─ urls.py

First, I added profile application and put user's link handle there.

profile/urls.py:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf.urls import url

from profile.views import show_my_page, show_feed, show_messages, show_friends, show_home_or_feed

urlpatterns = [
    url(r'^feed/$', show_feed, name='feed'),  # User feed
    url(r'^messages/$', show_messages, name='messages'), # User messages
    url(r'^friends/$', show_friends, name='friends'), # User friends
    ...

    url(r'^$', show_home_or_feed), # Main page or user feed
    url(r'^(?P<id_or_slug>[\w-]+)/$', show_my_page, name='my_page'),  # User page
]

Note the line url(r'^$', show_home_or_feed),. show_home_or_feed view define to show user feed, if he is registered, or wellcome page, created in the usual way in Django CMS.

profile/views.py:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from cms.views import details
from django.conf import settings
from django.contrib.auth import get_user_model
from django.shortcuts import render, redirect

User = get_user_model()


# get_user() method tries to find user in DB by id or slug. If user does not exists, then None is returned
def get_user(id_or_slug):
    try:
        if id_or_slug.startswith('user-'):
            user_id = id_or_slug.split('-')[1]
            try:
                return User.objects.get(id=user_id)
            except ValueError:
                return None
        else:
            return User.objects.get(slug=id_or_slug)
    except User.DoesNotExist:
        return None


def show_my_page(request, id_or_slug):
    user = get_user(id_or_slug)  # trying to define user by id_or_slug

    if not user:
        # If there is no such user, we try handle url with django-cms through its details(request, slug) view.

        # For me, links of main page of different language such as levels.pro/ru/ returned 404 code error, because id_or_slug equals /ru/, 
        # and request.path equals levels.pro/ru/, so Django cannot find page.
        # For fixing it enough just to clear id_or_slug.
        if id_or_slug in dict(settings.LANGUAGES).keys():
            id_or_slug = ''

        return details(request, id_or_slug)

    return render(request, 'profile/my_page.html', {'shown_user': user})


def show_feed(request):
    return render(request, 'profile/feed.html')


def show_home_or_feed(request):

    # This is special a way, that allows to edit the main cms-page.
    # As it was said above, when user is authorized, he redirects to feed.
    # But how can admin add content to main page? For it I use next a way - 
    # if url contains edit_main GET-parameter (for example, levels.pro/?edit_main),
    # then link will be handled by view of django-cms.
    if 'edit_main' in request.GET or not request.user.is_authenticated:
        return details(request, '')

    return redirect(show_feed)


def show_messages(request):
    return render(request, 'profile/messages.html')


def show_friends(request):
    return render(request, 'profile/friends.html')

urls.py:

from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static

from django.contrib import admin
admin.autodiscover()

...

i18n_urls = (
    url(r'^admin/', include(admin.site.urls)),
    ...
)

profile_urls = [
    url(r'^', include('profile.urls')),
]

cms_urls = [
    url(r'^', include('cms.urls')),
]


urlpatterns = []
urlpatterns.extend(i18n_patterns(*i18n_urls, prefix_default_language=False))
urlpatterns.extend(profile_urls)
urlpatterns.extend(i18n_patterns(*cms_urls, prefix_default_language=False))
urlpatterns.extend(static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT))

Idea is that urls profile application should not be built with language prefix, this is a first. And secondly, url(r'^', include('profile.urls')), line shouldn't be above, than url(r'^', include('cms.urls')),. It's need to be able to view of profile app to handle, for example, such url as levels.pro/vivazzi, namely, to determine whether it is slug of user or usual cms page.

Here I get difficulty. My trying to use profile_urls above, then all other urls:

# -----------------------------
# Does NOT work correctly!!!
# -----------------------------

from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static

from django.contrib import admin
admin.autodiscover()

...

profile_urls = [
    url(r'^', include('profile.urls')),
]

i18n_urls = (
    url(r'^admin/', include(admin.site.urls)),
    ...

    url(r'^', include('cms.urls')),
)


urlpatterns = []
urlpatterns.extend(profile_urls)
urlpatterns.extend(i18n_patterns(*i18n_urls, prefix_default_language=False))
urlpatterns.extend(static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT))

... fails, since admin urls are no longer loaded, so the order of urls is important.

After, I added support of multiple languages for the site. This is well described in the article Мультиязычность и перевод в Django. In principle, the settings are same as in article, except the one thing: it's need custom Middleware to determine the language by url. Default 'django.middleware.locale.LocaleMiddleware' is not suitable, because when trying to go to eg levels.pro/friends/ django will think, if there is no language prefix, then use English as current language (English is default: LANGUAGE_CODE = 'en'), but we need that language is not defined by such method for these urls. To solve this problem, I took as basis Django's LocaleMiddleware and inherited from it, overridden process_request method:

from cms.models import Title
from django.conf import settings
from django.conf.urls.i18n import is_language_prefix_patterns_used
from django.middleware.locale import LocaleMiddleware
from django.utils import translation


class CustomLocaleMiddleware(LocaleMiddleware):
    def process_request(self, request):
        urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF)
        i18n_patterns_used, prefixed_default_language = is_language_prefix_patterns_used(urlconf)
        language = translation.get_language_from_request(request, check_path=i18n_patterns_used)
        language_from_path = translation.get_language_from_path(request.path_info)
        if 'lang' in request.GET:
            language = request.GET['lang']
        elif not language_from_path and i18n_patterns_used and not prefixed_default_language:
            titles = Title.objects.filter(page__depth=1).distinct().values_list('slug', flat=True)
            titles = list('/{}/'.format(t) for t in titles)
            titles += ('/admin/', )

            if request.path.startswith(tuple(titles)):
                language = settings.LANGUAGE_CODE

        translation.activate(language)
        request.LANGUAGE_CODE = translation.get_language()

Since we doesn't use language prefix in user links such as levels.pro/friends/, then we cannot define language from link. Here I found one way: to change language to the default (in this case, English) only if url, i. e. request.path, matches the slug of cms pages. For example, /about/ is in ['/about/', '/terms/', ...] list of titles, so this url with the missing prefix tell that it's need to show English version of "About company" page (accordingly for /ru/about/ Russian version of page will be shown). But, for example, /vivazzi/ is not in list of titles. It follows that this user page and the language does not need to be switched.

Also I had to add /admin/ link in titles to work the redirect to admin panel. While working like this.

One more thing: if there is lang in request.GET, then we switch language. It's need when we are, for example, on page levels.pro/friends/ and we need to switch from English to Russian. We don't have levels.pro/ru/friends/ link for the reasons described above, so it's need somehow to send information about language change. For this I added lang GET-parameter to language switch template:

{% load i18n menu_tags %}
{% spaceless %}
    {% for language in languages %}
        <a href="{% page_language_url language.0 %}?lang={{ language.0 }}"{% if current_language == language.0 %} class="active"{% endif %} title="{% trans 'Change language to' %} {{ language.1 }}">{{ language.0|upper }}</a>
    {% endfor %}
{% endspaceless %}

And after I check for lang in GET-parameters of request:

if 'lang' in request.GET:
    language = request.GET['lang']

My settings.py for functionality of this miltulanguage site:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

...

TIME_ZONE = 'Asia/Irkutsk'
LANGUAGE_CODE = 'en'

LANGUAGES = (('en', 'English'), ('ru', 'Russian'))
LOCALE_PATHS = ('locale', )

PARLER_LANGUAGES = {
    1: (
        {'code': 'en'},
        {'code': 'ru'},
    ),
    'default': {
        'fallback': 'en',             
        'hide_untranslated': False,
    }
}

USE_I18N = True
USE_L10N = True
USE_TZ = True

MIDDLEWARE_CLASSES = (
    'django.middleware.cache.UpdateCacheMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.common.BrokenLinkEmailsMiddleware',

    'spec.middleware.CustomLocaleMiddleware',

    'django.middleware.common.CommonMiddleware',
    'cms.middleware.user.CurrentUserMiddleware',
    'cms.middleware.page.CurrentPageMiddleware',
    'cms.middleware.toolbar.ToolbarMiddleware',
    'cms.middleware.language.LanguageCookieMiddleware',

    'cms.middleware.utils.ApphookReloadMiddleware',

    'django.middleware.cache.FetchFromCacheMiddleware',
)

...

That's all that I wanted to tell you about the adding multilingualism in Levels project. There are a lot of subtleties. I hope, that I explain clearly the nuances of adding more complex translation logic.

I feel that there are many moments in code, that are needed in improvement. For example, thisTitle.objects.filter(page__depth=1).distinct().values_list('slug', flat=True) request. It should be cached so that the database does not jeck every time.

Over time, of course, this code will be supplemented. I think it's good beginning.

I'm glad that you have read this voluminous article! I wait for your comment to discuss the code.

Rate this article

5 from 5 (total 1 rating)

You can send feedback, suggestions or comments on this article using this form:

Fields marked by star ( * ) is required.

Thank you for yor feedback!

After clicking the "Send" button, your message will be delivered to me on the mail.

Author of Article

Artem Maltsev

Web-developer, having the knowlenge of programming language - Python, framework - Django, content management system - Django CMS, platform of e-commerce site - Django Shop and many other applications, using this technologies.

The right to use content on this page https://vivazzi.pro/it/levels/translation/:

Permission is granted to copy an content with its author and reference to the original without using the parameter rel="nofollow" in tag <a>. Usage:

Author of Article: Artem Maltsev
Link to article: <a href="https://vivazzi.pro/it/levels/translation/">https://vivazzi.pro/it/levels/translation/</a>

More: Terms of site usage

Comments: 0

You can leave a comment as an unregistered user.

But if you sing up, you can:

  • receive notifications
  • view your comments
  • be able to use all the functions of the developed services

To comment in one's own name you should log in or sign up on Vuspace website

Send

There is no search on this site, so I offer to use usual search engine, for example, Google, adding "vivazzi" after your request.

Try it

Select currency for displaying monetary values