Интернационализация Django

Django поддерживает перевод приложений на множество языков. Правильная настройка i18n позволяет создавать многоязычные приложения с поддержкой различных локалей, форматирования дат, чисел и валют.

Основы интернационализации

Интернационализация (i18n) включает в себя:

  • Перевод текста - локализация строк интерфейса
  • Форматирование дат - различные форматы дат по локалям
  • Форматирование чисел - разделители тысяч и десятичных знаков
  • Форматирование валют - символы валют и их позиции
  • Локализация адресов - различные форматы адресов
  • Правосторонние языки - поддержка арабского, иврита

Настройка i18n в settings.py

 1# settings.py
 2
 3# Основные настройки интернационализации
 4USE_I18N = True
 5USE_L10N = True  # Локализация
 6USE_TZ = True    # Временные зоны
 7
 8# Язык по умолчанию
 9LANGUAGE_CODE = 'ru-ru'
10
11# Поддерживаемые языки
12LANGUAGES = [
13    ('en', 'English'),
14    ('ru', 'Русский'),
15    ('de', 'Deutsch'),
16    ('fr', 'Français'),
17    ('es', 'Español'),
18    ('zh-cn', '中文'),
19    ('ja', '日本語'),
20    ('ar', 'العربية'),
21]
22
23# Локали для форматирования
24LOCALE_PATHS = [
25    os.path.join(BASE_DIR, 'locale'),
26    os.path.join(BASE_DIR, 'apps', 'users', 'locale'),
27    os.path.join(BASE_DIR, 'apps', 'board', 'locale'),
28]
29
30# Настройки локализации
31LANGUAGES_BIDI = {
32    'ar': True,   # Арабский - правосторонний
33    'he': True,   # Иврит - правосторонний
34    'fa': True,   # Фарси - правосторонний
35}
36
37# Форматы дат по локалям
38DATE_FORMAT = 'd.m.Y'
39DATETIME_FORMAT = 'd.m.Y H:i'
40SHORT_DATE_FORMAT = 'd.m.Y'
41SHORT_DATETIME_FORMAT = 'd.m.Y H:i'
42
43# Форматы чисел
44DECIMAL_SEPARATOR = ','
45THOUSAND_SEPARATOR = ' '
46NUMBER_GROUPING = 3
47
48# Настройки валют
49CURRENCY_FORMAT = 'N $'
50
51# Middleware для определения языка
52MIDDLEWARE = [
53    'django.middleware.security.SecurityMiddleware',
54    'django.contrib.sessions.middleware.SessionMiddleware',
55    'django.middleware.locale.LocaleMiddleware',  # Важно!
56    'django.middleware.common.CommonMiddleware',
57    'django.middleware.csrf.CsrfViewMiddleware',
58    'django.contrib.auth.middleware.AuthenticationMiddleware',
59    'django.contrib.messages.middleware.MessageMiddleware',
60    'django.middleware.clickjacking.XFrameOptionsMiddleware',
61]

Создание и управление переводами

  1# Создание файлов переводов
  2# python manage.py makemessages -l ru
  3# python manage.py makemessages -l en
  4# python manage.py makemessages -l de
  5
  6# Компиляция переводов
  7# python manage.py compilemessages
  8
  9# Пример .po файла (locale/ru/LC_MESSAGES/django.po)
 10"""
 11# Russian translation for Django project
 12# Copyright (C) 2025 ORGANIZATION
 13# This file is distributed under the same license as the Django project.
 14msgid ""
 15msgstr ""
 16"Project-Id-Version: Django project\n"
 17"Report-Msgid-Bugs-To: \n"
 18"POT-Creation-Date: 2025-01-01 12:00+0000\n"
 19"PO-Revision-Date: 2025-01-01 12:00+0000\n"
 20"Last-Translator: \n"
 21"Language: ru\n"
 22"MIME-Version: 1.0\n"
 23"Content-Type: text/plain; charset=UTF-8\n"
 24"Content-Transfer-Encoding: 8bit\n"
 25"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
 26
 27msgid "Welcome to our site"
 28msgstr "Добро пожаловать на наш сайт"
 29
 30msgid "Book"
 31msgid_plural "Books"
 32msgstr[0] "Книга"
 33msgstr[1] "Книги"
 34msgstr[2] "Книг"
 35
 36msgid "You have %(count)s unread message"
 37msgid_plural "You have %(count)s unread messages"
 38msgstr[0] "У вас %(count)s непрочитанное сообщение"
 39msgstr[1] "У вас %(count)s непрочитанных сообщения"
 40msgstr[2] "У вас %(count)s непрочитанных сообщений"
 41"""
 42
 43# Использование переводов в Python коде
 44from django.utils.translation import gettext as _
 45from django.utils.translation import gettext_lazy as _lazy
 46from django.utils.translation import ngettext
 47from django.utils.translation import pgettext
 48
 49# Простой перевод
 50message = _('Welcome to our site')
 51
 52# Ленивый перевод (для моделей)
 53class Book(models.Model):
 54    title = models.CharField(_lazy('Title'), max_length=200)
 55    description = models.TextField(_lazy('Description'))
 56
 57    class Meta:
 58        verbose_name = _lazy('Book')
 59        verbose_name_plural = _lazy('Books')
 60
 61# Множественные формы
 62def get_message_count(count):
 63    return ngettext(
 64        'You have %(count)s unread message',
 65        'You have %(count)s unread messages',
 66        count
 67    ) % {'count': count}
 68
 69# Контекстные переводы
 70def get_menu_item(context):
 71    if context == 'navigation':
 72        return pgettext('navigation', 'Home')
 73    elif context == 'page_title':
 74        return pgettext('page_title', 'Home')
 75    else:
 76        return _('Home')
 77
 78# Переводы в views
 79from django.shortcuts import render
 80from django.utils.translation import gettext as _
 81
 82def book_list(request):
 83    context = {
 84        'title': _('Book Catalog'),
 85        'description': _('Browse our collection of books'),
 86        'welcome_message': _('Welcome to our book store'),
 87    }
 88    return render(request, 'books/book_list.html', context)
 89
 90# Переводы в forms
 91from django import forms
 92from django.utils.translation import gettext_lazy as _lazy
 93
 94class BookForm(forms.ModelForm):
 95    class Meta:
 96        model = Book
 97        fields = ['title', 'description', 'author', 'category']
 98        labels = {
 99            'title': _lazy('Book Title'),
100            'description': _lazy('Description'),
101            'author': _lazy('Author'),
102            'category': _lazy('Category'),
103        }
104        help_texts = {
105            'title': _lazy('Enter the full title of the book'),
106            'description': _lazy('Provide a brief description of the book'),
107        }
108        error_messages = {
109            'title': {
110                'required': _lazy('Book title is required'),
111                'max_length': _lazy('Title is too long'),
112            },
113        }

Переводы в шаблонах

  1# base.html
  2{% load i18n %}
  3<!DOCTYPE html>
  4<html lang="{{ LANGUAGE_CODE }}">
  5<head>
  6    <meta charset="UTF-8">
  7    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  8    <title>{% block title %}{% endblock %}</title>
  9</head>
 10<body>
 11    <header>
 12        <nav>
 13            <ul>
 14                <li><a href="{% url 'home' %}">{% trans "Home" %}</a></li>
 15                <li><a href="{% url 'books' %}">{% trans "Books" %}</a></li>
 16                <li><a href="{% url 'about' %}">{% trans "About" %}</a></li>
 17                <li><a href="{% url 'contact' %}">{% trans "Contact" %}</a></li>
 18            </ul>
 19        </nav>
 20
 21        <!-- Переключатель языков -->
 22        <div class="language-switcher">
 23            <form action="{% url 'set_language' %}" method="post">
 24                {% csrf_token %}
 25                <input name="next" type="hidden" value="{{ request.path }}">
 26                <select name="language" onchange="this.form.submit()">
 27                    {% get_current_language as CURRENT_LANGUAGE %}
 28                    {% get_available_languages as LANGUAGES %}
 29                    {% get_language_info_list for LANGUAGES as languages %}
 30                    {% for language in languages %}
 31                        <option value="{{ language.code }}"
 32                                {% if language.code == CURRENT_LANGUAGE %}selected{% endif %}>
 33                            {{ language.name_local }}
 34                        </option>
 35                    {% endfor %}
 36                </select>
 37            </form>
 38        </div>
 39    </header>
 40
 41    <main>
 42        {% block content %}{% endblock %}
 43    </main>
 44
 45    <footer>
 46        <p>{% trans "All rights reserved" %} &copy; 2025</p>
 47    </footer>
 48</body>
 49</html>
 50
 51# book_list.html
 52{% extends 'base.html' %}
 53{% load i18n %}
 54
 55{% block title %}{% trans "Book Catalog" %}{% endblock %}
 56
 57{% block content %}
 58    <h1>{% trans "Book Catalog" %}</h1>
 59    <p>{% trans "Browse our collection of books" %}</p>
 60
 61    {% if books %}
 62        <div class="book-grid">
 63            {% for book in books %}
 64                <div class="book-card">
 65                    <h3>{{ book.title }}</h3>
 66                    <p>{{ book.description|truncatewords:20 }}</p>
 67                    <p><strong>{% trans "Author" %}:</strong> {{ book.author.name }}</p>
 68                    <p><strong>{% trans "Price" %}:</strong> {{ book.price|floatformat:2 }}</p>
 69                    <p><strong>{% trans "Published" %}:</strong> {{ book.published_date|date:"d.m.Y" }}</p>
 70                </div>
 71            {% endfor %}
 72        </div>
 73    {% else %}
 74        <p>{% trans "No books available" %}</p>
 75    {% endif %}
 76
 77    <!-- Пагинация -->
 78    {% if is_paginated %}
 79        <div class="pagination">
 80            {% if page_obj.has_previous %}
 81                <a href="?page={{ page_obj.previous_page_number }}">{% trans "Previous" %}</a>
 82            {% endif %}
 83
 84            <span>{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}</span>
 85
 86            {% if page_obj.has_next %}
 87                <a href="?page={{ page_obj.next_page_number }}">{% trans "Next" %}</a>
 88            {% endif %}
 89        </div>
 90    {% endif %}
 91{% endblock %}
 92
 93# Формы с переводами
 94# book_form.html
 95{% extends 'base.html' %}
 96{% load i18n %}
 97
 98{% block title %}{% trans "Add Book" %}{% endblock %}
 99
100{% block content %}
101    <h1>{% trans "Add New Book" %}</h1>
102
103    <form method="post">
104        {% csrf_token %}
105
106        <div class="form-group">
107            <label for="{{ form.title.id_for_label }}">{% trans "Title" %}:</label>
108            {{ form.title }}
109            {% if form.title.errors %}
110                <div class="error">
111                    {% for error in form.title.errors %}
112                        <span>{% trans error %}</span>
113                    {% endfor %}
114                </div>
115            {% endif %}
116            {% if form.title.help_text %}
117                <small>{% trans form.title.help_text %}</small>
118            {% endif %}
119        </div>
120
121        <div class="form-group">
122            <label for="{{ form.description.id_for_label }}">{% trans "Description" %}:</label>
123            {{ form.description }}
124            {% if form.description.errors %}
125                <div class="error">
126                    {% for error in form.description.errors %}
127                        <span>{% trans error %}</span>
128                    {% endfor %}
129                </div>
130            {% endif %}
131        </div>
132
133        <button type="submit">{% trans "Save Book" %}</button>
134    </form>
135{% endblock %}

Форматирование дат и чисел

  1# Форматирование дат по локалям
  2from django.utils import timezone
  3from django.utils.translation import get_language
  4from django.template.defaultfilters import date, time
  5
  6# В Python коде
  7def format_date_examples():
  8    now = timezone.now()
  9
 10    # Различные форматы дат
 11    formats = {
 12        'ru': {
 13            'short': now.strftime('%d.%m.%Y'),
 14            'long': now.strftime('%d %B %Y года'),
 15            'time': now.strftime('%H:%M'),
 16            'datetime': now.strftime('%d.%m.%Y %H:%M'),
 17        },
 18        'en': {
 19            'short': now.strftime('%m/%d/%Y'),
 20            'long': now.strftime('%B %d, %Y'),
 21            'time': now.strftime('%I:%M %p'),
 22            'datetime': now.strftime('%m/%d/%Y %I:%M %p'),
 23        },
 24        'de': {
 25            'short': now.strftime('%d.%m.%Y'),
 26            'long': now.strftime('%d. %B %Y'),
 27            'time': now.strftime('%H:%M'),
 28            'datetime': now.strftime('%d.%m.%Y %H:%M'),
 29        }
 30    }
 31
 32    current_lang = get_language()
 33    return formats.get(current_lang, formats['en'])
 34
 35# В шаблонах
 36"""
 37{% load i18n %}
 38{% load l10n %}
 39
 40<!-- Форматирование дат -->
 41<p>{% trans "Today" %}: {{ today|date:"d.m.Y" }}</p>
 42<p>{% trans "Current time" %}: {{ now|time:"H:i" }}</p>
 43
 44<!-- Локализованное форматирование -->
 45<p>{% trans "Published" %}: {{ book.published_date|date:"SHORT_DATE_FORMAT" }}</p>
 46<p>{% trans "Created" %}: {{ book.created_at|date:"DATETIME_FORMAT" }}</p>
 47
 48<!-- Форматирование чисел -->
 49<p>{% trans "Price" %}: {{ book.price|floatformat:2 }}</p>
 50<p>{% trans "Rating" %}: {{ book.rating|floatformat:1 }}</p>
 51"""
 52
 53# Кастомные фильтры для форматирования
 54from django import template
 55from django.utils.translation import get_language
 56from django.template.defaultfilters import floatformat
 57
 58register = template.Library()
 59
 60@register.filter
 61def currency(value, currency_code='RUB'):
 62    """Форматирование валюты по локали"""
 63    if value is None:
 64        return ''
 65
 66    lang = get_language()
 67
 68    # Форматы валют по локалям
 69    currency_formats = {
 70        'ru': {
 71            'RUB': '{:,.2f} ₽',
 72            'USD': '{:,.2f} $',
 73            'EUR': '{:,.2f} €',
 74        },
 75        'en': {
 76            'RUB': '₽{:,.2f}',
 77            'USD': '${:,.2f}',
 78            'EUR': '€{:,.2f}',
 79        },
 80        'de': {
 81            'RUB': '{:,.2f} ₽',
 82            'USD': '{:,.2f} $',
 83            'EUR': '{:,.2f} €',
 84        }
 85    }
 86
 87    format_template = currency_formats.get(lang, currency_formats['en']).get(currency_code, '{:,.2f}')
 88    return format_template.format(float(value))
 89
 90@register.filter
 91def localize_number(value):
 92    """Локализация чисел (разделители)"""
 93    if value is None:
 94        return ''
 95
 96    lang = get_language()
 97
 98    # Разделители по локалям
 99    separators = {
100        'ru': {'decimal': ',', 'thousand': ' '},
101        'de': {'decimal': ',', 'thousand': '.'},
102        'en': {'decimal': '.', 'thousand': ','},
103    }
104
105    sep = separators.get(lang, separators['en'])
106
107    if isinstance(value, (int, float)):
108        # Форматируем число с разделителями
109        formatted = f"{value:,.2f}" if isinstance(value, float) else f"{value:,}"
110
111        if lang == 'ru':
112            formatted = formatted.replace(',', ' ').replace('.', ',')
113        elif lang == 'de':
114            formatted = formatted.replace(',', '.').replace('.', ',')
115
116        return formatted
117
118    return str(value)

Middleware и определение языка

 1# Кастомное middleware для определения языка
 2from django.utils.deprecation import MiddlewareMixin
 3from django.utils import translation
 4from django.conf import settings
 5from django.http import HttpResponseRedirect
 6from django.urls import reverse
 7
 8class CustomLocaleMiddleware(MiddlewareMixin):
 9    """Middleware для определения языка пользователя"""
10
11    def process_request(self, request):
12        # Приоритет определения языка:
13        # 1. Язык из URL
14        # 2. Язык из сессии
15        # 3. Язык из cookie
16        # 4. Язык из заголовка Accept-Language
17        # 5. Язык по умолчанию
18
19        # Проверяем язык в URL
20        path = request.path_info.lstrip('/')
21        if path and path.split('/')[0] in dict(settings.LANGUAGES):
22            language = path.split('/')[0]
23            translation.activate(language)
24            request.LANGUAGE_CODE = language
25            return
26
27        # Проверяем язык в сессии
28        if hasattr(request, 'session'):
29            language = request.session.get('django_language')
30            if language and language in dict(settings.LANGUAGES):
31                translation.activate(language)
32                request.LANGUAGE_CODE = language
33                return
34
35        # Проверяем язык в cookie
36        language = request.COOKIES.get('django_language')
37        if language and language in dict(settings.LANGUAGES):
38            translation.activate(language)
39            request.LANGUAGE_CODE = language
40            return
41
42        # Проверяем заголовок Accept-Language
43        if 'HTTP_ACCEPT_LANGUAGE' in request.META:
44            accept_lang = request.META['HTTP_ACCEPT_LANGUAGE']
45            for lang_code, lang_name in settings.LANGUAGES:
46                if lang_code in accept_lang:
47                    translation.activate(lang_code)
48                    request.LANGUAGE_CODE = lang_code
49                    return
50
51        # Используем язык по умолчанию
52        translation.activate(settings.LANGUAGE_CODE)
53        request.LANGUAGE_CODE = settings.LANGUAGE_CODE
54
55# View для смены языка
56from django.shortcuts import redirect
57from django.utils import translation
58from django.conf import settings
59
60def set_language(request):
61    """View для смены языка"""
62    if request.method == 'POST':
63        language = request.POST.get('language')
64        if language and language in dict(settings.LANGUAGES):
65            # Активируем язык
66            translation.activate(language)
67
68            # Сохраняем в сессии
69            if hasattr(request, 'session'):
70                request.session['django_language'] = language
71
72            # Сохраняем в cookie
73            response = redirect(request.POST.get('next', '/'))
74            response.set_cookie('django_language', language, max_age=365*24*60*60)
75
76            return response
77
78    return redirect('/')
79
80# URL для смены языка
81# urls.py
82from django.urls import path
83from .views import set_language
84
85urlpatterns = [
86    path('i18n/setlang/', set_language, name='set_language'),
87    # ... другие URL
88]

Переводы в API и сериализаторах

  1# Сериализаторы с переводами
  2from rest_framework import serializers
  3from django.utils.translation import gettext_lazy as _lazy
  4from .models import Book, Category
  5
  6class CategorySerializer(serializers.ModelSerializer):
  7    class Meta:
  8        model = Category
  9        fields = ['id', 'name', 'description']
 10
 11    def to_representation(self, instance):
 12        """Переводим названия категорий"""
 13        data = super().to_representation(instance)
 14
 15        # Получаем текущий язык
 16        language = self.context['request'].LANGUAGE_CODE
 17
 18        # Если есть переводы для категории
 19        if hasattr(instance, 'translations'):
 20            translation = instance.translations.filter(language=language).first()
 21            if translation:
 22                data['name'] = translation.name
 23                data['description'] = translation.description
 24
 25        return data
 26
 27class BookSerializer(serializers.ModelSerializer):
 28    category = CategorySerializer(read_only=True)
 29    author_name = serializers.CharField(source='author.name', read_only=True)
 30
 31    class Meta:
 32        model = Book
 33        fields = [
 34            'id', 'title', 'description', 'author_name', 'category',
 35            'price', 'rating', 'published_date', 'is_available'
 36        ]
 37
 38    def to_representation(self, instance):
 39        """Переводим данные книги"""
 40        data = super().to_representation(instance)
 41
 42        language = self.context['request'].LANGUAGE_CODE
 43
 44        # Переводим заголовок и описание
 45        if hasattr(instance, 'translations'):
 46            translation = instance.translations.filter(language=language).first()
 47            if translation:
 48                data['title'] = translation.title
 49                data['description'] = translation.description
 50
 51        # Форматируем дату по локали
 52        if instance.published_date:
 53            data['published_date'] = instance.published_date.strftime('%d.%m.%Y')
 54
 55        # Форматируем цену по локали
 56        if instance.price:
 57            data['price'] = f"{instance.price:,.2f}"
 58
 59        return data
 60
 61# API views с переводами
 62from rest_framework import viewsets
 63from rest_framework.decorators import action
 64from rest_framework.response import Response
 65from django.utils.translation import gettext as _
 66
 67class BookViewSet(viewsets.ModelViewSet):
 68    queryset = Book.objects.all()
 69    serializer_class = BookSerializer
 70
 71    @action(detail=False, methods=['get'])
 72    def search(self, request):
 73        """Поиск книг с переводами"""
 74        query = request.query_params.get('q', '')
 75        if not query:
 76            return Response({
 77                'error': _('Search query is required')
 78            })
 79
 80        books = Book.objects.filter(
 81            Q(title__icontains=query) |
 82            Q(description__icontains=query) |
 83            Q(author__name__icontains=query)
 84        )
 85
 86        serializer = self.get_serializer(books, many=True)
 87        return Response({
 88            'query': query,
 89            'count': books.count(),
 90            'results': serializer.data,
 91            'message': _('Found %(count)s books') % {'count': books.count()}
 92        })
 93
 94    @action(detail=False, methods=['get'])
 95    def categories(self, request):
 96        """Список категорий с переводами"""
 97        categories = Category.objects.all()
 98        serializer = CategorySerializer(categories, many=True, context={'request': request})
 99
100        return Response({
101            'categories': serializer.data,
102            'total': categories.count(),
103            'message': _('Available categories')
104        })
105
106# API документация с переводами
107from drf_spectacular.utils import extend_schema, OpenApiParameter
108from drf_spectacular.types import OpenApiTypes
109
110class BookViewSet(viewsets.ModelViewSet):
111    @extend_schema(
112        summary=_('List all books'),
113        description=_('Retrieve a list of all available books with translations'),
114        parameters=[
115            OpenApiParameter(
116                name='language',
117                type=OpenApiTypes.STR,
118                location=OpenApiParameter.HEADER,
119                description=_('Language code for translations')
120            ),
121        ],
122        responses={200: BookSerializer(many=True)}
123    )
124    def list(self, request, *args, **kwargs):
125        return super().list(request, *args, **kwargs)

Тестирование интернационализации

  1# Тесты для переводов
  2from django.test import TestCase, override_settings
  3from django.utils.translation import activate, deactivate
  4from django.urls import reverse
  5from .models import Book, Category
  6
  7class I18nTestCase(TestCase):
  8    def setUp(self):
  9        """Подготовка тестовых данных"""
 10        self.category = Category.objects.create(
 11            name='Test Category',
 12            description='Test category description'
 13        )
 14
 15        self.book = Book.objects.create(
 16            title='Test Book',
 17            description='Test book description',
 18            category=self.category,
 19            price=100.00
 20        )
 21
 22    def test_english_translation(self):
 23        """Тест английского перевода"""
 24        activate('en')
 25
 26        response = self.client.get(reverse('book_list'))
 27        self.assertEqual(response.status_code, 200)
 28
 29        # Проверяем, что текст на английском
 30        self.assertContains(response, 'Book Catalog')
 31        self.assertContains(response, 'Add Book')
 32        self.assertContains(response, 'Title')
 33        self.assertContains(response, 'Description')
 34
 35    def test_russian_translation(self):
 36        """Тест русского перевода"""
 37        activate('ru')
 38
 39        response = self.client.get(reverse('book_list'))
 40        self.assertEqual(response.status_code, 200)
 41
 42        # Проверяем, что текст на русском
 43        self.assertContains(response, 'Каталог книг')
 44        self.assertContains(response, 'Добавить книгу')
 45        self.assertContains(response, 'Название')
 46        self.assertContains(response, 'Описание')
 47
 48    def test_language_switching(self):
 49        """Тест переключения языков"""
 50        # Начинаем с английского
 51        activate('en')
 52
 53        # Переключаемся на русский
 54        response = self.client.post(reverse('set_language'), {
 55            'language': 'ru',
 56            'next': reverse('book_list')
 57        })
 58
 59        self.assertEqual(response.status_code, 302)
 60
 61        # Проверяем, что язык изменился
 62        response = self.client.get(reverse('book_list'))
 63        self.assertContains(response, 'Каталог книг')
 64
 65    def test_date_formatting(self):
 66        """Тест форматирования дат"""
 67        activate('ru')
 68
 69        response = self.client.get(reverse('book_detail', args=[self.book.id]))
 70        self.assertEqual(response.status_code, 200)
 71
 72        # Проверяем формат даты (русский формат)
 73        self.assertContains(response, self.book.published_date.strftime('%d.%m.%Y'))
 74
 75    def test_number_formatting(self):
 76        """Тест форматирования чисел"""
 77        activate('ru')
 78
 79        response = self.client.get(reverse('book_detail', args=[self.book.id]))
 80        self.assertEqual(response.status_code, 200)
 81
 82        # Проверяем формат цены (русский формат)
 83        self.assertContains(response, '100,00')
 84
 85# Тесты с переопределением настроек
 86@override_settings(LANGUAGES=[('en', 'English'), ('ru', 'Русский')])
 87class I18nSettingsTestCase(TestCase):
 88    def test_language_settings(self):
 89        """Тест настроек языков"""
 90        from django.conf import settings
 91        self.assertIn(('en', 'English'), settings.LANGUAGES)
 92        self.assertIn(('ru', 'Русский'), settings.LANGUAGES)
 93
 94# Тесты для API
 95class I18nAPITestCase(TestCase):
 96    def setUp(self):
 97        self.client.force_authenticate(user=self.user)
 98
 99    def test_api_translations(self):
100        """Тест переводов в API"""
101        # Тест с английским языком
102        response = self.client.get(
103            reverse('api:book-list'),
104            HTTP_ACCEPT_LANGUAGE='en'
105        )
106        self.assertEqual(response.status_code, 200)
107        self.assertIn('Book Catalog', response.data.get('message', ''))
108
109        # Тест с русским языком
110        response = self.client.get(
111            reverse('api:book-list'),
112            HTTP_ACCEPT_LANGUAGE='ru'
113        )
114        self.assertEqual(response.status_code, 200)
115        self.assertIn('Каталог книг', response.data.get('message', ''))

Лучшие практики

  • Используй gettext_lazy для моделей - избегай проблем с переводами при загрузке
  • Группируй переводы по приложениям - создавай отдельные .po файлы для каждого приложения
  • Используй контекстные переводы - pgettext для слов с разным значением в разных контекстах
  • Тестируй переводы на всех языках - проверяй корректность отображения
  • Кешируй переводы - используй Redis для хранения скомпилированных переводов
  • Создавай fallback переводы - обеспечивай отображение на языке по умолчанию
  • Используй правильные форматы дат - учитывай локальные особенности
  • Тестируй правосторонние языки - проверяй корректность отображения RTL
  • Документируй переводы - создавай глоссарий терминов для переводчиков
  • Мониторь качество переводов - собирай обратную связь от пользователей

FAQ

Q: Как создать файлы переводов?
A: Используй python manage.py makemessages -l ru для создания .po файлов, затем редактируй их и компилируй с помощью python manage.py compilemessages.

Q: В чем разница между gettext и gettext_lazy?
A: gettext выполняет перевод сразу, gettext_lazy откладывает перевод до момента использования, что важно для моделей и форм.

Q: Как форматировать даты по локалям?
A: Используй фильтры date и time в шаблонах, или создавай кастомные фильтры для специфичного форматирования.

Q: Можно ли использовать переводы в API?
A: Да, можно переводить сообщения об ошибках, описания полей и другие тексты в API ответах.

Q: Как тестировать переводы?
A: Создавай тесты с активацией различных языков, проверяй корректность отображения текста и форматирования.

Q: Что делать, если перевод не работает?
A: Проверь настройки LANGUAGES, LOCALE_PATHS, убедись что LocaleMiddleware добавлен в MIDDLEWARE, проверь что .po файлы скомпилированы.

Q: Как добавить новый язык?
A: Добавь код языка в LANGUAGES, создай папку locale/код_языка/LC_MESSAGES/, создай .po файл с помощью makemessages, переведи тексты и скомпилируй.