
Здравствуйте. На днях возникла задача русифицировать админку django включая названия моделей, полей и приложений. Основной целью было избежать модифицирования кода django. Продолжительное гугление не дало целостной картины этого процесса. Поэтому я решил собрать все в одном месте.
Сразу скажу, что еще в самом начале проекта поставил django-admin-tools и тем самым сохранил себе некоторое количество нервных клеток. А все манипуляции проводились над django 1.3.
Подготовка
Для начала пропишем в конфигурационном файле
LANGUAGE_CODE = 'ru-RU' USE_I18N = True
Затем создадим свои классы для приборной панели и меню django-admin-tools. Для этого выполним последовательно команды
python manage.py custommenupython 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 для автоматизации всех приведенных выше действий.