
Здравствуйте. На днях возникла задача русифицировать админку django включая названия моделей, полей и приложений. Основной целью было избежать модифицирования кода django. Продолжительное гугление не дало целостной картины этого процесса. Поэтому я решил собрать все в одном месте.
Сразу скажу, что еще в самом начале проекта поставил django-admin-tools и тем самым сохранил себе некоторое количество нервных клеток. А все манипуляции проводились над django 1.3.
Подготовка
Для начала пропишем в конфигурационном файле
LANGUAGE_CODE = 'ru-RU'
USE_I18N = True
Затем создадим свои классы для приборной панели и меню django-admin-tools. Для этого выполним последовательно команды
python manage.py custommenu
python manage.py customdashboard
В результате выполнения этих комманд у вас в корневой директории проекта появятся два файла dashboard.py и menu.py. Далее в конфигурационном файле проекта нужно указать где находятся нужные классы. Для этого допишем в него следующие строчки
ADMIN_TOOLS_MENU = 'myproject.menu.CustomMenu'
ADMIN_TOOLS_INDEX_DASHBOARD = 'myproject.dashboard.CustomIndexDashboard'
ADMIN_TOOLS_APP_INDEX_DASHBOARD = 'myproject.dashboard.CustomAppIndexDashboard'
Путь может быть любой. Главное чтоб по нему находились нужные классы
Для перевода нам понадобится утилита gettext. Её установка отличается для разных систем. Поэтому углубляться в сей процесс не будем. Работать будем с кодировкой utf-8.
Gettext использует для перевода словари с раширением .po, которые переводит в бинарный формат с расширением .mo. Для того чтоб их подготовить нужно в корневой директории проекта или приложения создать папку locale. Именно папку, а не модуль python. То есть без файла __init__.py иначе будут ошибки.
Далее нужно открыть консоль и перейти в ту директорию в которую положили папку locale и выполнить команду
python manage.py makemessages -l ru
При выполнении этой команды будут просканированы все файлы на предмет обращения к словарю и составлен файл django.po, который появится в папке locale/ru/LC_MESSAGES. Можно выполнять эту команду регулярно после добавлений новых обращений к словарю в коде или же править файл django.po руками.
Чтоб изменения в словаре вступили в силу нужно выполнить команду
python manage.py compilemessages
после завершения которой рядом с файлом django.po появится django.mo.
Перевод имени приложения
Первым делом нужно заставить админку отображать русское название для имени приложения. На одном из форумов советовали просто прописать в поле app_label подкласса Meta в модели нужное значение, но от этого я отказался сразу. Так как меняется url приложения и с syncdb начались проблемы. Перекрытие метода title у str тоже не помогло так как слетал фильтр и admin-tools начинал лепить все модели в один бокс.
Я обычно запускаю команду makemessages в процессе работы над проектом, а значит нам нужно место, где будет обозначено обращение к словарю. Проще говоря я вписываю в файл __init__.py своего приложения следующий код
from django.utils.translation import ugettext_lazy as _
_('Feedback')
Здесь мы импортируем модуль ugettext_lazy и делаем обращение к словарю за переводом. Если после этого запустить команду makemessages еще раз, то в файл django.po будут добавлены следующие строки
#: feedback/__init__.py:2
msgid "Feedback"
msgstr ""
и мы сможем подставить в msgstr свой перевод. В данном случае «Обратная связь». Теперь нам нужно сделать так чтоб при отображении шаблона названия приложения бралось из нашего словаря. Для этого сначала переопределим шаблон app_list.html. Этот шаблон используется при выводе модуля AppList.
В нашей директории templates создадим определенную структуру директорий и положим туда файл app_list.html так чтоб у нас получился путь
templates/admin_tools/dashboard/modules/add_list.html
Этот файл должен иметь то же содержание что и оригинальный app_list.html. Теперь изменим код в строке 5 на следующий
<h3><a href="{{ child.url }}">{% trans child.title %}</a></h3>
Таким образом при отображении названия приложения в общем списке будет браться наше значение из словаря.
В общем списке название отображается нормально, но когда мы заходим в само приложение, то заголовок модуля все еще не переведен. Для того чтоб исправить это заглянем в наш файл dashboard.py, который мы создавали в начале, и найдем там класс CustomAppIndexDashboard. Он отвечает за формирования страницы приложения в админке. В его методе __init__ исправим код чтоб получить следующее
self.children += [
modules.ModelList(_(self.app_title), self.models),
#... дальше оставляем все как было
Здесь мы завернули self.app_title в функцию ugettext_lazy и теперь на странице приложения название будет так же переведено.
Остались только хлебные крошки. Там по-прежнему отображается оригинальное название.
Модуль breadcrumbs используется в большом количестве шаблонов, поэтому за мыслями я полез потрошить файлы django.contrib.admin. Результатом чего стал вот такой класс. Его надо прописать в файле admin.py вашего приложения до регистрации модулей админки. Забегая вперед скажу, что здесь мы так же переводим и заголовки страниц просмотра, редактирования и добавления модели c помощью библиотеки, о которой расскажу чуть ниже.
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from django.utils.text import capfirst
from django.db.models.base import ModelBase
from django.conf import settings
from pymorphy import get_morph
morph = get_morph(settings.PYMORPHY_DICTS['ru']['dir'])
class I18nLabel():
def __init__(self, function):
self.target = function
self.app_label = u''
def rename(self, f, name = u''):
def wrapper(*args, **kwargs):
extra_context = kwargs.get('extra_context', {})
if 'delete_view' != f.__name__:
extra_context['title'] = self.get_title_by_name(f.__name__, args[1], name)
else:
extra_context['object_name'] = morph.inflect_ru(name, u'вн').lower()
kwargs['extra_context'] = extra_context
return f(*args, **kwargs)
return wrapper
def get_title_by_name(self, name, request={}, obj_name = u''):
if 'add_view' == name:
return _('Add %s') % morph.inflect_ru(obj_name, u'вн,стр').lower()
elif 'change_view' == name:
return _('Change %s') % morph.inflect_ru(obj_name, u'вн,стр').lower()
elif 'changelist_view' == name:
if 'pop' in request.GET:
title = _('Select %s')
else:
title = _('Select %s to change')
return title % morph.inflect_ru(obj_name, u'вн,стр').lower()
else:
return ''
def wrapper_register(self, model_or_iterable, admin_class=None, **option):
if isinstance(model_or_iterable, ModelBase):
model_or_iterable = [model_or_iterable]
for model in model_or_iterable:
if admin_class is None:
admin_class = type(model.__name__+'Admin', (admin.ModelAdmin,), {})
self.app_label = model._meta.app_label
current_name = model._meta.verbose_name.upper()
admin_class.add_view = self.rename(admin_class.add_view, current_name)
admin_class.change_view = self.rename(admin_class.change_view, current_name)
admin_class.changelist_view = self.rename(admin_class.changelist_view, current_name)
admin_class.delete_view = self.rename(admin_class.delete_view, current_name)
return self.target(model, admin_class, **option)
def wrapper_app_index(self, request, app_label, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context['title'] = _('%s administration') % _(capfirst(app_label))
return self.target(request, app_label, extra_context)
def register(self):
return self.wrapper_register
def index(self):
return self.wrapper_app_index
admin.site.register = I18nLabel(admin.site.register).register()
admin.site.app_index = I18nLabel(admin.site.app_index).index()
При помощи него мы заменяем контекст для рендеринга шаблона на вызов функции ugettext_lazy. Таким образом мы перевели название приложения в хлебных крошках и заголовке страницы. Но это еще не все. Для полноты картины нам надо перегрузить еще один шаблон admin/app_index.html И строку 11 заменим на
{% trans app.name %}
Осталось только перевести имя приложения в выпадающем меню. Для этого достаточно перегрузить шаблон admin_tools/menu/item.html и поправить пару строк. В блок load второй строки добавляем i18n, а в конец 5й строки вместо {{ item.title }} пишем {% trans item.title %}.
Теперь все названия нашего приложения будут отображаться из словаря django.mo. Можем идти дальше
Перевод названия модели и полей
Если название приложения нам нужно просто выводить в переведенном виде, то название модели хорошо бы выводить с учетом падежа, рода и числа. В поисках красивого решения я наткнулся на великолепный модуль pymorphy от kmike, за который ему огромное спасибо. Он очень удобен в использовании и прекрасно делает свою работу! К тому же для админки нам большая скорость не нужна. Все что нам остается — это установить модуль pymorphy и интегрировать его в django руководствуясь шагами из документации.
Теперь нам нужно переопределить несколько шаблонов в админке и расставить там фильтры pymorphy при этом все переводы строк должны оставаться в одном месте. А именно в файле django.po.
Дальше будем для примера русифицировать модель Picture чтоб она отображалась как «Картинка». Первым делом в этой модели пропишем
class Picture(models.Model):
title = models.CharField(max_length=255, verbose_name=_('title'))
...
class Meta:
verbose_name = _(u'picture')
verbose_name_plural = _(u'pictures')
И добавим в файл django.po
msgid "picture"
msgstr "картинка"
msgid "pictures"
msgstr "картинки"
msgid "title"
msgstr "заголовок"
Теперь осталось сделать чтоб переведенные слова отображались с учетом падежа и числа
Начнем с шаблона admin/change_list.html. Он отвечает за вывод списка элементов модели. Для начала добавим в блок load модуль pymorphy_tags. Например в строку 2. Чтоб получилось
{% load adminmedia admin_list i18n pymorphy_tags %}
Далее находим там строку 64, которая отвечает за вывод кнопки добавления
{% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}
и меняем её на
{% blocktrans with cl.opts.verbose_name|inflect:"вн" as name %}Add {{ name }}{% endblocktrans %}
Здесь мы добавили изменение названия модели в винительный падеж. И получили правильную надпись «Добавить картинку». Подробнее о формах изменения можно почитать здесь.
Заголовки страниц уже переведены в нужной форме с помощью класса I18nLabel так что можно двигаться дальше.
Теперь перегрузим шаблон admin/change_form.html. Сначала нужно добавить модуль pymorphy_tags в блок load, а затем исправить там хлебные крошки заменив в строке 22
{{ opts.verbose_name }}
на
{{ opts.verbose_name|inflect:"вн" }}
Далее во списку идет шаблон admin/delete_selected_confirmation.html. В нем все правки делаем тем же способом, что и в предыдущих случаях. Здесь нужно сначала исправить хлебные крошки вот так
{% trans app_label|capfirst %}
К сожалению функция delete_selected, которая отвечает за вывод этой страницы не поддерживает extra_context, что меня очень печалит. Поэтому я сделал свой фильтр, который изменяет форму числа в зависимости от величины объекта.
from django import template
from django.conf import settings
from pymorphy import get_morph
register = template.Library()
morph = get_morph(settings.PYMORPHY_DICTS['ru']['dir'])
@register.filter
def plural_from_object(source, object):
l = len(object[0])
if 1 == l:
return source
return morph.pluralize_inflected_ru(source.upper(), l).lower()
Теперь во всех местах надо расширить блок blocktrans примерно так
{% blocktrans %}Are you sure you want to delete the selected {{ objects_name }}? All of the following objects and their related items will be deleted:{% endblocktrans %}
исправить на
{% blocktrans with objects_name|inflect:"вн"|plural_from_object:deletable_objects as objects_name %}Are you sure you want to delete the selected {{ objects_name }}? All of the following objects and their related items will be deleted:{% endblocktrans %}
После всего этого остается лишь перегрузить шаблон admin/pagination.html, подключить в нем модуль pymorphy_tags и заменить в нем строку 9 на
{{ cl.result_count }} {{ cl.opts.verbose_name|lower|plural:cl.result_count }}
фильтр lower я добавил потому что возникала ошибка при преобразовании прокси объекта gettext в фильтре plural. Но, возможно, это у меня в окружении такой глюк и у вас не будет необходимости его добавлять.
Следующий по плану шаблон admin/filter.html Тут просто первые две строки заменить на
{% load i18n pymorphy_tags %}
<h3>{% blocktrans with title|inflect:"дт" as filter_title %} By {{ filter_title }} {% endblocktrans %}</h3>
Остались только пользовательские сообщения, которые все еще выводятся без учета числа. Для того чтоб исправить эту досадную несправедливость нужно переопределить метод message_user у класса ModelAdmin. Можно вставить это в admin.py. У меня получилось делать следующим образом
def message_wrapper(f):
def wrapper(self, request, message):
gram_info = morph.get_graminfo( self.model._meta.verbose_name.upper() )[0]
if -1 != message.find(u'"'):
"""
Message about some action with a single element
"""
words = [w for w in re.split("( |\\\".*?\\\".*?)", message) if w.strip()]
form = gram_info['info'][:gram_info['info'].find(',')]
message = u' '.join(words[:2])
for word in words[2:]:
if not word.isdigit():
word = word.replace(".", "").upper()
try:
info = morph.get_graminfo(word)[0]
if u'КР_ПРИЛ' != info['class']:
word = morph.inflect_ru(word, form).lower()
elif 0 <= info['info'].find(u'мр'):
word = morph.inflect_ru(word, form, u'КР_ПРИЧАСТИЕ').lower()
else:
word = word.lower()
except IndexError:
word = word.lower()
message += u' ' + word
else:
"""
Message about some action with a group of elements
"""
num = int(re.search("\d", message).group(0))
words = message.split(u' ')
message = words[0]
pos = gram_info['info'].find(',')
form = gram_info['info'][:pos] + u',' + u'ед' if 1 == num else u'мн'
for word in words[1:]:
if not word.isdigit():
word = word.replace(".", "").upper()
info = morph.get_graminfo(word)[0]
if u'КР_ПРИЛ' != info['class']:
word = morph.pluralize_inflected_ru(word, num).lower()
else:
word = morph.inflect_ru(word, form, u'КР_ПРИЧАСТИЕ').lower()
message += u' ' + word
message += '.'
return f(self, request, capfirst(message))
return wrapper
admin.ModelAdmin.message_user = message_wrapper(admin.ModelAdmin.message_user)
Здесь мы разбираем сообщение по словам и склоняем их в нужную форму. Отдельно отличаются сообщения для групп объектов и для единиц.
Вот теперь мы можем наблюдать примерно такую картинку

Заключение
Отсутствие предусмотренных решений в архитектуре django, безусловно, расстраивает, но все в наших руках. Возможно некоторые решения могут показаться вам кривыми, но я пока не нашел способа сделать это изящнее.
При написании статьи я старался изложить кратко и по пунктам, и не смотря на количество текста получается не так много движений для достижения результата. Это при условии использования приведенного выше кода.
Основная цель этой работы была перевести административный интерфейс и сохранить все переведенные строки в одном месте, а именно в языковом файле. Что мы и получили.
Буду благодарен за любые замечания и предложения. Спасибо за внимание.
P.S. Уже готовые шаблоны вы можете забрать здесь. Вам нужно будет распаковать содержимое архива в вашу директорию templates.
[UPD]: Михаил (kmike) завел проект на bitbucket под названием django-russian-admin для автоматизации всех приведенных выше действий.