1. Поддержка нескольких языков сайта

13 сентября 2017 г. 22:12

Первая задача в Levels, которая быстро не решалась - поддержка нескольких языков сайта, причём нужно, чтобы часть url-ов по обычному строилась для разных языков сайта, то есть с подставлением префикса языка перед url, а другая часть url-ов не содержала эти префиксы. Для наглядности приведу часть ссылок:

levels.pro/about/ - "О компании" на английском языке
levels.pro/ru/about/ - "О компании" на русском языке
levels.pro/de/about/ - "О компании" на немецком языке
levels.pro/messages/ - сообщения пользователя 
levels.pro/friends/ - список друзей пользователя

Ссылки levels.pro/about/, levels.pro/ru/about/, levels.pro/de/about/, как вы видите, содержат информацию о компании на английском, русском и немецком языках соответственно. Языковой префикс перед /about/ подсказывает нам, на каком языке будет представлена информация.

А вот для ссылок levels.pro/messages/ и levels.pro/friends/ не нужен языковой префикс, потому что это ссылки конкретного авторизованного пользователя сайта. Подобные ссылки лучше делать одинаковыми (без использования префикса) вне зависимости от выбранного языка - это удобный и красивый вид представления url.

Но для продвижения сайта в поисковых системах информационные ссылки типа "О компании" лучше сделать отдельными страницами для каждого языка. В итоге личные разделы пользователя (Сообщения, Друзья и т. д.) закрыты от поисковых систем, а информация "О компании", "Реклама" и т. д. открыта и будет индексироваться поисковыми системами.

Также стоит обратить внимание на "главную ссылку", то есть на levels.pro/. Для неавторизованных пользователей нужно показывать приветственную страницу сайта с предложением войти или зарегистрироваться. Но если пользователь вошёл в систему, то ему должна открыться, допустим, его личная страница или лента новостей.

Поняв логику построения url-ов, перейдём к реализации.

Структура проекта

Для наглядности приведу частичную структуру проекта, которая есть на момент написания статьи:

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

Для начала я создал приложение profile и поместил туда обработку ссылок пользователя.

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'),  # Новости пользователя
    url(r'^messages/$', show_messages, name='messages'), # Сообщения пользователя
    url(r'^friends/$', show_friends, name='friends'), # Друзья пользователя
    ...

    url(r'^$', show_home_or_feed), # Главная страница или Новости пользователя
    url(r'^(?P<id_or_slug>[\w-]+)/$', show_my_page, name='my_page'),  # страница пользователя
]

Обратите внимание на строчку url(r'^$', show_home_or_feed),. Представление show_home_or_feed как раз таки и будет определять показывать новости пользователя, если он зарегистрирован, или приветственную страницу, созданную обычным способом в 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() пытается найти пользователя в БД по id или slug. Если такого пользователя нет, то возвращается None
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)  # пытаемся определить пользователя по id_or_slug

    if not user:
        # если такого пользователя нет, то пробуем обработать url самой django-cms через её представление details(request, slug).

        # У меня ссылки главной страницы разных языков типа levels.pro/ru/ показывали 404 ошибку, так как id_or_slug был равен /ru/, 
        # да ещё и сам request.path был равен levels.pro/ru/, поэтому Django не мог найти страницу.
        # Чтобы поправить это дело, достаточно просто очистить 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):

    # Это специальный трюк, который позволяет редактировать главную cms-страницу.
    # Как было сказано выше, когда пользователь авторизован, то его перенаправляет на новости.
    # Но как тогда admin-у добавить содержание на главную страницу? Вот для этого я придумал следующее - 
    # если в ссылке есть GET-параметр edit_main (например, levels.pro/?edit_main),
    # то обрабатывать ссылку представлением приложения 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))

Фишка в том, что url-ы приложения profile не должны строиться с языковым префиксом, о чём я говорил выше - это во-первых. А во-вторых, строчка url(r'^', include('profile.urls')), не должна быть выше, чем url(r'^', include('cms.urls')),, чтобы иметь возможность представлению приложения profile обработать, например, такого рода url как levels.pro/vivazzi, а именно определить это slug пользователя или же обычная cms-страница.

Здесь у меня и начались трудности. Попытка расположить сначала profile_urls, а затем все остальные url-ы:

# -----------------------------
# НЕ работающий как надо код!!!
# -----------------------------

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))

... провалилась, так как админские ссылки перестали загружаться, поэтому порядок следования url важен.

Далее, я добавил поддержку нескольких языков для сайта. Это хорошо описано в статье Мультиязычность и перевод в Django . В принципе, настройки такие же как и в этой статье, за исключением одного момента: требуется кастомный милдварь для определения языка по url. Дефолтный 'django.middleware.locale.LocaleMiddleware' не подходит, так как при попытке перейти, к примеру, по адресу levels.pro/friends/ django будет думать, что раз языкового префикса нет, то значит текущим языком поставить английский (англ. язык стоит по умолчанию: LANGUAGE_CODE = 'en'), а нам нужно, чтобы для этих ссылок по такому принципу не определялся язык. Чтобы решить этот вопрос, я взял за основу джанговский LocaleMiddleware и унаследовался от него, переопределив метод process_request:

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()

Так как мы не используем языковой префикс в пользовательских ссылках типа levels.pro/friends/, то и нет возможности из ссылки определить язык. Здесь я поступил следующим образом. Весь смысл в том, чтобы менять язык на дефолтный (в данном случае на английский) только в том случае, если адрес страницы, то есть request.path, будет совпадать со slug-ами cms-страниц. К примеру, /about/ есть в списке названий страниц ['/about/', '/terms/', ...], поэтому данный url с отсутствующим префиксом говорит о том, что нужно показать английскую версию страницы "О компании" (соответственно для /ru/about/ будет показываться русская версия страницы). А например, /vivazzi/ - такого slug-а нет в списке названий страниц. Отсюда следует, что это пользовательская страница и язык не нужно переключать.

Также мне пришлось добавить ссылку /admin/ в переменную titles для работоспособности перехода в админку. Пока работает так.

Ещё один момент: если в request.GET есть параметр lang, то мы однозначно переключаем язык. Это нужно, когда мы находимся, допустим, на странице levels.pro/friends/ и нам нужно переключить с английского языка на русский. У нас нет ссылки levels.pro/ru/friends/ по причинам, описанным выше, поэтому надо как-то по-другому передать информацию о смене языка. Для этого я добавил GET-параметр lang в шаблон переключения языка:

{% 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 %}

А затем проверяю наличие lang в GET-параметрах запроса:

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

Представлю получившийся settings.py для работоспособности мультиязычности сайта:

# -*- 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',
)

...

Вроде бы и всё, с чем я хотел с вами поделиться по поводу добавления многоязычности в проект Levels. Тонкостей много, надеюсь, что доходчиво рассказал о нюансах добавления более сложной логики перевода.

Чувствую, что много есть моментов в коде, где можно было бы улучшить. Первое, что бросается в глаза, вот этот запрос Title.objects.filter(page__depth=1).distinct().values_list('slug', flat=True) - его нужно кешировать, чтобы каждый раз базу не дёргать.

Со временем, конечно, этот код будет дополняться, может ещё что-то вылезет, но, на мой взгляд, уже есть хорошее начало.

Рад, что вы дочитали эту объёмную статью! Жду ваших комментариев - обсудим код.

Оцените статью

0 из 5 (всего 0 оценок)

Поля, отмеченные звёздочкой ( * ) , являются обязательными.

Спасибо за ваш отзыв!

Автор статьи

Права на использование материала, расположенного на этой странице http://vivazzi.ru/it/levels/translation/:

Разрешается копировать материал с указанием её автора и ссылки на оригинал без использования параметра rel="nofollow" в теге <a>. Использование:

Автор статьи: Мальцев Артём
Ссылка на статью: <a href="http://vivazzi.ru/it/levels/translation/">http://vivazzi.ru/it/levels/translation/</a>

Подробнее: Правила использования сайта

Комментариев: 0

Вы можете оставить комментарий как незарегистрированный пользователь. Но, зарегистрировавшись, вы сможете получать оповещения об ответах, а также иметь доступ к своему личному аккаунту для просмотра своих комментариев.

Чтобы оставить комментарий от своего имени войдите или зарегистрируйтесь обычным способом или через социальные сети:

Отправить

На данный момент нет специального поиска, поэтому я предлагаю воспользоваться обычной поисковой системой, например, Google, добавив "vivazzi" после своего запроса.

Попробуйте