1. Поддержка нескольких языков сайта
14 сентября 2017 г. 2: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