Интернационализация 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" %} © 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, переведи тексты и скомпилируй.