Full Russification admin area


    Hello. The other day there was a task to Russify the django admin area including the names of models, fields and applications. The main goal was to avoid modifying django code. Prolonged googling did not give a holistic picture of this process. Therefore, I decided to collect everything in one place.
    I must say right away that at the very beginning of the project I installed django-admin-tools and thereby saved a certain number of nerve cells. And all the manipulations were carried out on django 1.3.


    Training


    First, write in the configuration file

    LANGUAGE_CODE = 'ru-RU'
    USE_I18N = True
    


    Then create your classes for the dashboard and django-admin-tools menu. To do this, execute the commands sequentially.

    python manage.py custommenu
    python manage.py customdashboard

    As a result of these commands, two files dashboard.py and menu.py will appear in the root directory of the project. Next, in the project configuration file, you need to specify where the necessary classes are located. To do this, add the following lines to it

    ADMIN_TOOLS_MENU = 'myproject.menu.CustomMenu'
    ADMIN_TOOLS_INDEX_DASHBOARD = 'myproject.dashboard.CustomIndexDashboard'
    ADMIN_TOOLS_APP_INDEX_DASHBOARD = 'myproject.dashboard.CustomAppIndexDashboard'
    

    The path can be any. The main thing is that the necessary classes are found on it.

    For the translation we need the gettext utility . Its installation is different for different systems. Therefore, we will not delve into this process. We will work with utf-8 encoding.
    Gettext uses dictionaries with the extension .po for translation, which translates into binary format with the extension .mo. In order to prepare them, you need to create the locale folder in the root directory of the project or application. It is a folder, not a python module. That is, without the __init__.py file, there will otherwise be errors.
    Next, open the console and go to the directory in which you put the locale folder and execute the command

    python manage.py makemessages -l ru

    When this command is executed, all files will be scanned for access to the dictionary and the django.po file will be compiled, which will appear in the locale / ru / LC_MESSAGES folder. You can execute this command regularly after adding new entries to the dictionary in the code, or you can edit the django.po file by hand.
    In order for the changes in the dictionary to take effect, you need to run the command

    python manage.py compilemessages

    after which django.mo appears next to the django.po file.

    Application Name Translation


    First of all, you need to make the admin panel display the Russian name for the application name. At one of the forums, it was advised to simply enter the desired value in the app_label field of the Meta subclass in the model, but I refused it right away. Since the application url is changing and problems started with syncdb. Overriding the title method of str didn’t help either, as the filter flew and admin-tools began to sculpt all models in one box.
    I usually run the makemessages command while working on a project, which means we need a place where the dictionary access will be indicated. Simply put, I enter the following code into the __init__.py file of my application

    from django.utils.translation import ugettext_lazy as _
    _('Feedback')
    

    Here we import the ugettext_lazy module and make a reference to the dictionary for translation. If you then run the makemessages command again, the following lines will be added to the django.po file and we can substitute our translation in msgstr. In this case, “Feedback”. Now we need to make it so that when displaying the template, the application name is taken from our dictionary. To do this, first redefine the app_list.html template. This template is used in the output of the AppList module. In our templates directory, create a specific directory structure and put the app_list.html file there so that we get the path. This file should have the same content as the original app_list.html . Now change the code in line 5 to the next

    #: feedback/__init__.py:2
    msgid "Feedback"
    msgstr ""




    templates/admin_tools/dashboard/modules/add_list.html



    {% trans child.title %}


    Thus, when displaying the name of the application in the general list, our value from the dictionary will be taken.
    In the general list, the name is displayed normally, but when we go into the application itself, the module title is still not translated. To fix this, take a look at our dashboard.py file, which we created at the beginning, and find the CustomAppIndexDashboard class there. He is responsible for the formation of the application page in the admin panel. In his __init__ method, we fix the code to get the following

    self.children += [
        modules.ModelList(_(self.app_title), self.models),
        #... дальше оставляем все как было
    

    Here we wrapped self.app_title in the ugettext_lazy function and now on the application page the name will also be translated.
    Only bread crumbs remained. The original name is still displayed there.
    The breadcrumbs module is used in a large number of templates, so for my thoughts I got to gut the django.contrib.admin files. The result of what was such a class. It must be registered in the admin.py file of your application before registering the admin modules. Looking ahead, I’ll say that here we also translate the titles of the pages for viewing, editing and adding a model using the library, which I will talk about below.
    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()
    

    With it, we replace the context for rendering the template with a call to the ugettext_lazy function. Thus, we translated the name of the application in breadcrumbs and page title. But that is not all. To complete the picture, we need to reload another admin / app_index.html template and replace line 11 with

    {% trans app.name %}
    

    It remains only to translate the name of the application in the drop-down menu. To do this, just overload the admin_tools / menu / item.html template and fix a couple of lines. Add i18n to the load block of the second line, and at the end of the 5th line we write {% trans item.title%} instead of {{item.title}}.
    Now all the names of our application will be displayed from the django.mo dictionary. We can go further

    Translation of model name and fields


    If the name of the application we just need to display in the translated form, then the name of the model would be good to display taking into account the case, gender and number. In search of a beautiful solution, I came across a great pymorphy module from kmike , for which many thanks to him. It is very convenient to use and does a great job! In addition, we do not need a high speed for the admin panel. All that remains for us is to install the pymorphy module and integrate it into django guided by the steps from the documentation.
    Now we need to redefine several templates in the admin panel and place pymorphy filters there, while all line feeds should remain in one place. Namely, in the django.po file.
    Further, for example, we will Russify the Picture model so that it is displayed as a “Picture”. The first thing in this model is to write

    class Picture(models.Model):
        title = models.CharField(max_length=255, verbose_name=_('title'))
        ...
        class Meta:
            verbose_name = _(u'picture')
            verbose_name_plural = _(u'pictures')
    

    And add to the django.po file. Now it remains to be done so that the translated words are displayed taking into account the case and number. Let's start with the admin / change_list.html template . He is responsible for listing the model elements. First, add the pymorphy_tags module to the load block. For example, in line 2. So that it happens. Next we find line 64 there, which is responsible for the output of the add button and change it to Here we added a change in the model name to the accusative case. And they got the correct inscription “Add a picture”. Read more about the forms of change here . The page headers are already translated in the right form using the I18nLabel class so that you can move on. Now reload the template

    msgid "picture"
    msgstr "картинка"

    msgid "pictures"
    msgstr "картинки"

    msgid "title"
    msgstr "заголовок"




    {% load adminmedia admin_list i18n pymorphy_tags %}



    {% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}

    {% blocktrans with cl.opts.verbose_name|inflect:"вн" as name %}Add {{ name }}{% endblocktrans %}



    admin / change_form.html . First you need to add the pymorphy_tags module to the load block, and then fix the breadcrumbs there by replacing line 22

    {{ opts.verbose_name }}
    with
    {{ opts.verbose_name|inflect:"вн" }}

    Next . The admin / delete_selected_confirmation.html template is on the list . In it, we make all the changes in the same way as in previous cases. Here you need to first fix the breadcrumbs like this

    {% trans app_label|capfirst %}

    Unfortunately the delete_selected function, which is responsible for the output of this page does not support extra_context, which saddens me very much. Therefore, I made my own filter, which changes the shape of the number depending on the size of the object.

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


    Now in all places you need to expand the blocktrans block like this to

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

    after all that remains is to reload the admin / pagination.html template , connect the pymorphy_tags module in it and replace line 9 in it with

    {{ cl.result_count }} {{ cl.opts.verbose_name|lower|plural:cl.result_count }}

    the lower filter, I added because an error occurred while converting the proxy of the gettext object in the plural filter. But perhaps this is surrounded by such a glitch and you will not need to add it.

    The next template is admin / filter.html. Here, just the first two lines are replaced by

    {% load i18n pymorphy_tags %}

    {% blocktrans with title|inflect:"дт" as filter_title %} By {{ filter_title }} {% endblocktrans %}




    There are only user messages that are still displayed without taking into account the number. In order to fix this annoying injustice, you need to override the message_user method of the ModelAdmin class. You can paste this into admin.py. I managed to do as follows

    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)
    

    Here we parse the message according to the words and incline them into the desired form. Separately differ messages for groups of objects and for units.

    Now we can observe something like this


    Conclusion


    The lack of solutions in the django architecture is certainly frustrating, but everything is in our hands. Perhaps some solutions may seem crooked to you, but I have not yet found a way to make it more elegant.
    When writing an article, I tried to summarize briefly and point by point, and in spite of the amount of text, there are not many movements to achieve the result. This is subject to the use of the above code.

    The main goal of this work was to translate the administrative interface and save all translated lines in one place, namely in a language file. Which we got.

    I would be grateful for any comments and suggestions. Thanks for attention.

    PS You can pick up ready-made templates here.. You will need to unzip the contents of the archive into your templates directory.

    [UPD]: Michael ( kmike ) started a bitbucket project called django-russian-admin to automate all of the above.

    Also popular now: