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