📜 ⬆️ ⬇️

Full Russification admin


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


Training


To begin with, we write in the configuration file

LANGUAGE_CODE = 'ru-RU' USE_I18N = True 

')
Then create your own django-admin-tools dashboard classes. To do this, execute the commands

python manage.py custommenu
python manage.py customdashboard

As a result of these commands, you will have two dashboard.py and menu.py files 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, we add to it the following lines

 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 that it was on the right classes

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

python manage.py makemessages -l ru

When executing this command, all files will be scanned for access to the dictionary and the django.po file compiled, which will appear in the folder locale / en / LC_MESSAGES. You can execute this command regularly after adding new references to the dictionary in the code, or edit the django.po file with your hands.
To make changes in the dictionary take effect, you need to execute the command

python manage.py compilemessages

after completion of which, django.mo appears next to the django.po file.

Translation of the application name


First of all, you need to force the admin panel to display the Russian name for the application name. At one of the forums, it was advised to simply write the desired value in the app_label field of the Meta subclass in the model, but I refused this right away. As the url of the application changes and problems start with syncdb. The overlap of the title method in str did not help either, since the filter flew in and the admin-tools began to sculpt all the models in one box.
I usually run the makemessages command while working on a project, which means we need a place where the dictionary reference will be indicated. Simply put, I enter the following code in 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 an appeal to the dictionary for translation. If you then run the makemessages command again, the following lines will be added to the django.po file

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

and we will be able to substitute your translation in msgstr. In this case, "Feedback". Now we need to make it so that when displaying the template the name of the application is taken from our dictionary. To do this, first override the app_list.html template. This template is used when displaying the AppList module.
In our templates directory, we will create a specific directory structure and put the app_list.html file there so that we have a way

templates/admin_tools/dashboard/modules/add_list.html

This file should have the same content as the original app_list.html . Now change the code in line 5 to the following

 <h3><a href="{{ child.url }}">{% trans child.title %}</a></h3> 

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

 self.children += [ modules.ModelList(_(self.app_title), self.models), #...      

Here we wrapped the self.app_title function in the ugettext_lazy function and now the name will also be translated on the application page.
Only bread crumbs remained. It still displays the original name.
The breadcrumbs module is used in a large number of templates, so for my thoughts I’ve got to gut django.contrib.admin files. The result of which was such a class. It must be registered in the admin.py file of your application before registering admin modules. Looking ahead, I’ll say that here we also translate the page headers for viewing, editing and adding a model using the library, which I’ll 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 the function call ugettext_lazy. So we translated the name of the application in breadcrumbs and the title of the page. 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 application name in the drop-down menu. To do this, simply reload the admin_tools / menu / item.html template and fix a couple of lines. In the load block of the second line, add i18n, and at the end of the 5th line, instead of {{item.title}} we write {% trans item.title%}.
Now all the names of our application will be displayed from the django.mo dictionary. We can go further

Translation of the model name and fields


If the name of the application, we just need to display in 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 magnificent kmike pymorphy module, for which I thank him so much. It is very easy to use and does its job perfectly! In addition, for the admin we do not need great speed. All we have to do is install the pymorphy module and integrate it into django following the steps in the documentation.
Now we need to redefine several templates in the admin panel and place pymorphy filters there, while all line breaks should remain in one place. Namely, in the file django.po.
Next, for example, we will Russify the Picture model so that it is displayed as a “Picture”. The first thing to do in this model is

 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 file django.po

msgid "picture"
msgstr ""

msgid "pictures"
msgstr ""

msgid "title"
msgstr ""

Now it remains to be done so that the translated words are displayed taking into account the case and the number
Let's start with the admin / change_list.html template. He is responsible for displaying a list of model elements. To begin with, add the pymorphy_tags module to the load block. For example, in line 2. To get

{% load adminmedia admin_list i18n pymorphy_tags %}

Next we find there line 64, which is responsible for the output of the add button

{% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}
and change it to
{% blocktrans with cl.opts.verbose_name|inflect:"" as name %}Add {{ name }}{% endblocktrans %}

Here we added the change of the model name to the accusative case. And got the correct inscription "Add a picture." Read more about the forms of change can be read here .
Page headers are already translated in the right form using the I18nLabel class so you can move on.
Now we will overload the admin / change_form.html template . First you need to add the pymorphy_tags module to the load block, and then fix the bread crumbs there replacing in line 22

{{ opts.verbose_name }}
on
{{ opts.verbose_name|inflect:"" }}

Next in the list is the admin / delete_selected_confirmation.html template . All edits are made in it in the same way as in the previous cases. Here you need to first fix the bread crumbs like this

{% trans app_label|capfirst %}

Unfortunately, the function delete_selected, which is responsible for displaying this page, does not support extra_context, which makes me very sad. 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 it is necessary to expand the blocktrans block like so

{% 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 on
{% 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 this, it remains only 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 }}

I added the lower filter because there was an error when converting the gettext proxy object in the plural filter. But perhaps this is in my environment such a glitch and you will not need to add it.

The following admin / filter.html template is planned to replace the first two lines with

{% load i18n pymorphy_tags %}
<h3>{% blocktrans with title|inflect:"" as filter_title %} By {{ filter_title }} {% endblocktrans %}</h3>


There are only user messages that are still displayed without counting the number. In order to correct this annoying injustice, you need to override the message_user method of the ModelAdmin class. You can paste this into admin.py. I did it like this

 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 sort the message by words and incline them to the desired form. Separately different messages for groups of objects and for units.

Now we can see 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 like curves to you, but I have not yet found a way to make it more elegant.
When writing an article, I tried to explain briefly and point by point, and in spite of the amount of text, there are not so many movements to achieve a 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 the translated strings in one place, namely in the language file. What 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 unpack 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 actions.

Source: https://habr.com/ru/post/125213/


All Articles