Добавление класса LocaleManager

This commit is contained in:
2026-07-10 23:02:17 +03:00
parent ef09fb4de3
commit 31a13c7ccc
6 changed files with 487 additions and 1 deletions

View File

@@ -21,3 +21,19 @@ class UILanguage(CoreEnum):
RUSSIAN = 'ru' RUSSIAN = 'ru'
""" Русский """ """ Русский """
@classmethod
def from_string(cls, value: str) -> UILanguage:
"""
Создает значение из строкового представления
:param value: Строковое значение
:return: Язык интерфейса пользователя
"""
normalized = value.lower().split('-')[0]
for member in cls:
if member.value == normalized:
return member
raise ValueError(f"Unsupported language: {value}")

View File

@@ -0,0 +1,11 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Пакет классов менеджеров
"""
from .locale_manager import LocaleManager

View File

@@ -0,0 +1,126 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса LocaleManager
"""
from django.conf import settings
from django.utils import translation
from rest_framework.request import Request
from net.xeaf.rack.enums import UILanguage
class LocaleManager:
"""
Реализует методы работы с локализацией
"""
UI_LANGUAGE_PARAM = "x-ui-language"
""" Идентификатор параметра языка интерфейса """
UI_LANGUAGE_META = "HTTP_ACCEPT_LANGUAGE"
""" Идентификатор заголовка языка интерфейса """
@classmethod
def get_default_language(cls) -> UILanguage:
"""
Возвращает языка интерфейса пользователя по умолчанию
:return: Язык интерфейса пользователя по умолчанию
"""
return UILanguage.from_string(settings.LANGUAGE_CODE)
@classmethod
def get_accepted_language(cls, language_code: str) -> UILanguage:
"""
Проверяет и возвращает допустимый код языка
:param language_code: Код языка интерфейса пользователя
:return: Допустимый код языка интерфейса пользователя
"""
try:
return UILanguage.from_string(language_code)
except ValueError:
return cls.get_default_language()
@classmethod
def get_request_language(cls, request: Request | None) -> UILanguage:
"""
Возвращает идентификатор языка из запроса
:param request: Информация о запросе
:return: Язык интерфейса пользователя из запроса
"""
if not request:
return cls.get_default_language()
if request.method == "GET":
language_code = request.query_params.get(cls.UI_LANGUAGE_PARAM, None)
if language_code:
return cls.get_accepted_language(language_code=language_code)
try:
language_code = cls.process_accepted_language(request)
return cls.get_accepted_language(language_code=language_code)
except KeyError:
return cls.get_default_language()
@classmethod
def set_request_language(cls, request: Request | None, ui_language: UILanguage) -> None:
"""
Задает язык интерфейса пользователя для запроса
:param request: Информация о запросе
:param ui_language: Язык интерфейса пользователя
"""
translation.activate(str(ui_language).lower())
if request:
request.LANGUAGE_CODE = str(translation.get_language()).upper()
@classmethod
def process_accepted_language(cls, request: Request) -> str:
"""
Обрабатывает значение META_ACCEPTED_LANGUAGE
:param request: Информация о запросе
:return: Код языка интерфейса пользователя из запроса
"""
index = -1
result = cls.get_default_language()
language = request.META[cls.UI_LANGUAGE_META]
for ui_lang in UILanguage.values():
lang = ui_lang.lower()
try:
idx = language.index(lang)
if index == -1 or idx < index:
index = idx
result = lang
except ValueError:
pass
return result
@classmethod
def adjust_request_language(cls, request: Request) -> None:
"""
Настраивает язык интерфейса пользователя
:param request: Информация о запросе
"""
language = cls.get_request_language(request)
cls.set_request_language(request, language)

View File

@@ -13,7 +13,7 @@ from django.test import TestCase
from net.xeaf.rack.enums import UILanguage from net.xeaf.rack.enums import UILanguage
class UILanguageTest(TestCase): class UILanguageTests(TestCase):
""" """
Тесты для перечисления UILanguage Тесты для перечисления UILanguage
""" """
@@ -34,3 +34,79 @@ class UILanguageTest(TestCase):
for code in UILanguage.values(): for code in UILanguage.values():
self.assertEqual(len(code), 2) self.assertEqual(len(code), 2)
self.assertTrue(code.islower()) self.assertTrue(code.islower())
def test_from_string_returns_english_for_en(self):
"""
Проверяем, что from_string возвращает ENGLISH для 'en'
"""
result = UILanguage.from_string('en')
self.assertEqual(result, UILanguage.ENGLISH)
def test_from_string_returns_russian_for_ru(self):
"""
Проверяем, что from_string возвращает RUSSIAN для 'ru'
"""
result = UILanguage.from_string('ru')
self.assertEqual(result, UILanguage.RUSSIAN)
def test_from_string_handles_uppercase(self):
"""
Проверяем, что from_string корректно обрабатывает заглавные буквы
"""
result = UILanguage.from_string('EN')
self.assertEqual(result, UILanguage.ENGLISH)
result = UILanguage.from_string('RU')
self.assertEqual(result, UILanguage.RUSSIAN)
def test_from_string_handles_regional_variants(self):
"""
Проверяем, что from_string корректно обрабатывает региональные варианты и лишние суффиксы
"""
test_cases = [
('en-us', UILanguage.ENGLISH),
('en-US', UILanguage.ENGLISH),
('en-gb', UILanguage.ENGLISH),
('en-GB', UILanguage.ENGLISH),
('en-us-extra', UILanguage.ENGLISH), # лишний суффикс
('en-us-extra-stuff', UILanguage.ENGLISH), # несколько суффиксов
('ru-ru', UILanguage.RUSSIAN),
('ru-RU', UILanguage.RUSSIAN),
]
for value, expected in test_cases:
with self.subTest(value=value):
result = UILanguage.from_string(value)
self.assertEqual(result, expected)
def test_from_string_raises_value_error_for_unknown(self):
"""
Проверяем, что from_string выбрасывает ValueError для неизвестного языка
"""
with self.assertRaises(ValueError):
UILanguage.from_string('fr')
with self.assertRaises(ValueError):
UILanguage.from_string('de')
with self.assertRaises(ValueError):
UILanguage.from_string('es')
def test_from_string_raises_value_error_for_invalid_format(self):
"""
Проверяем, что from_string выбрасывает ValueError для некорректного формата
"""
with self.assertRaises(ValueError):
UILanguage.from_string('123')
with self.assertRaises(ValueError):
UILanguage.from_string('')
with self.assertRaises(ValueError):
UILanguage.from_string('fr-FR') # 'fr' не поддерживается

View File

@@ -0,0 +1,12 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Пакет описания тестов для классов менеджеров
"""
from .locale_manager_tests import LocaleManagerTests

View File

@@ -0,0 +1,245 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: MIT
"""
Описание класса LocaleManagerTest
"""
from django.conf import settings
from django.test import TestCase
from django.utils import translation
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory
from net.xeaf.rack.enums import UILanguage
from net.xeaf.rack.managers import LocaleManager
class LocaleManagerTests(TestCase):
"""
Тесты для менеджера локализации
"""
def setUp(self):
"""
Подготовка данных для тестов
"""
self.factory = APIRequestFactory()
self.default_language = UILanguage.from_string(settings.LANGUAGE_CODE)
def test_get_default_language_returns_settings_value(self):
"""
Проверяем, что метод возвращает язык из настроек
"""
result = LocaleManager.get_default_language()
expected = UILanguage.from_string(settings.LANGUAGE_CODE)
self.assertEqual(result, expected)
def test_get_accepted_language_returns_english_for_en(self):
"""
Проверяем, что для 'en' возвращается UILanguage.ENGLISH
"""
result = LocaleManager.get_accepted_language('en')
self.assertEqual(result, UILanguage.ENGLISH)
def test_get_accepted_language_returns_russian_for_ru(self):
"""
Проверяем, что для 'ru' возвращается UILanguage.RUSSIAN
"""
result = LocaleManager.get_accepted_language('ru')
self.assertEqual(result, UILanguage.RUSSIAN)
def test_get_accepted_language_handles_uppercase(self):
"""
Проверяем, что метод корректно обрабатывает заглавные буквы
"""
result = LocaleManager.get_accepted_language('EN')
self.assertEqual(result, UILanguage.ENGLISH)
def test_get_accepted_language_returns_default_for_unknown(self):
"""
Проверяем, что для неизвестного кода возвращается язык по умолчанию
"""
result = LocaleManager.get_accepted_language('fr')
self.assertEqual(result, self.default_language)
def test_get_accepted_language_returns_default_for_empty_string(self):
"""
Проверяем, что для пустой строки возвращается язык по умолчанию
"""
result = LocaleManager.get_accepted_language('')
self.assertEqual(result, self.default_language)
def test_get_request_language_returns_default_for_none_request(self):
"""
Проверяем, что для None запроса возвращается язык по умолчанию
"""
result = LocaleManager.get_request_language(None)
self.assertEqual(result, self.default_language)
def test_get_request_language_from_query_param_on_get_request(self):
"""
Проверяем, что для GET запроса язык берется из query параметра
"""
request = self.factory.get('/?x-ui-language=ru')
request.method = 'GET'
request = Request(request)
result = LocaleManager.get_request_language(request)
self.assertEqual(result, UILanguage.RUSSIAN)
def test_get_request_language_ignores_query_param_on_non_get(self):
"""
Проверяем, что для не-GET запроса query параметр игнорируется
"""
request = self.factory.post('/?x-ui-language=ru')
request.method = 'POST'
request.META['HTTP_ACCEPT_LANGUAGE'] = 'en-US,en;q=0.9'
request = Request(request)
result = LocaleManager.get_request_language(request)
self.assertEqual(result, UILanguage.ENGLISH)
def test_get_request_language_from_accept_header(self):
"""
Проверяем, что язык берется из заголовка Accept-Language
"""
request = self.factory.get('/')
request.method = 'GET'
request.META['HTTP_ACCEPT_LANGUAGE'] = 'ru-RU,ru;q=0.9,en;q=0.8'
request = Request(request)
result = LocaleManager.get_request_language(request)
self.assertEqual(result, UILanguage.RUSSIAN)
def test_get_request_language_returns_default_if_header_missing(self):
"""
Проверяем, что при отсутствии заголовка возвращается язык по умолчанию
"""
request = self.factory.get('/')
request.method = 'GET'
request = Request(request)
result = LocaleManager.get_request_language(request)
self.assertEqual(result, self.default_language)
def test_get_request_language_prefers_query_param_over_header(self):
"""
Проверяем, что query параметр имеет приоритет над заголовком
"""
request = self.factory.get('/?x-ui-language=en')
request.method = 'GET'
request.META['HTTP_ACCEPT_LANGUAGE'] = 'ru-RU,ru;q=0.9'
request = Request(request)
result = LocaleManager.get_request_language(request)
self.assertEqual(result, UILanguage.ENGLISH)
def test_set_request_language_activates_translation(self):
"""
Проверяем, что метод активирует перевод для указанного языка
"""
request = self.factory.get('/')
request = Request(request)
LocaleManager.set_request_language(request, UILanguage.RUSSIAN)
from django.utils import translation
self.assertEqual(translation.get_language(), 'ru')
def test_set_request_language_sets_language_code_in_request(self):
"""
Проверяем, что метод устанавливает LANGUAGE_CODE в запросе
"""
request = self.factory.get('/')
request = Request(request)
LocaleManager.set_request_language(request, UILanguage.ENGLISH)
self.assertEqual(request.LANGUAGE_CODE, 'EN')
def test_set_request_language_handles_none_request(self):
"""
Проверяем, что метод корректно обрабатывает None в запросе
"""
LocaleManager.set_request_language(None, UILanguage.RUSSIAN)
self.assertEqual(translation.get_language(), 'ru')
def test_process_accepted_language_returns_first_matching_language(self):
"""
Проверяем, что метод возвращает первый подходящий язык из заголовка
"""
request = self.factory.get('/')
request.META['HTTP_ACCEPT_LANGUAGE'] = 'fr-FR,fr;q=0.9,ru-RU,ru;q=0.8,en;q=0.7'
request = Request(request)
result = LocaleManager.process_accepted_language(request)
self.assertEqual(result, 'ru')
def test_process_accepted_language_returns_default_if_no_match(self):
"""
Проверяем, что если ни один язык не найден, возвращается язык по умолчанию
"""
request = self.factory.get('/')
request.META['HTTP_ACCEPT_LANGUAGE'] = 'fr-FR,fr;q=0.9,de;q=0.8'
request = Request(request)
result = LocaleManager.process_accepted_language(request)
self.assertEqual(result, str(self.default_language).lower())
def test_process_accepted_language_raises_keyerror_if_header_missing(self):
"""
Проверяем, что при отсутствии заголовка выбрасывается KeyError
"""
request = self.factory.get('/')
request = Request(request)
with self.assertRaises(KeyError):
LocaleManager.process_accepted_language(request)
def test_adjust_request_language_sets_language_from_request(self):
"""
Проверяем, что метод настраивает язык на основе запроса
"""
request = self.factory.get('/?x-ui-language=ru')
request.method = 'GET'
request = Request(request)
LocaleManager.adjust_request_language(request)
from django.utils import translation
self.assertEqual(translation.get_language(), 'ru')
self.assertEqual(request.LANGUAGE_CODE, 'RU')
def test_adjust_request_language_uses_default_if_no_language_found(self):
"""
Проверяем, что если язык не найден, используется язык по умолчанию
"""
request = self.factory.get('/')
request.method = 'GET'
request = Request(request)
LocaleManager.adjust_request_language(request)
from django.utils import translation
expected = str(self.default_language).lower()
self.assertEqual(translation.get_language(), expected)
def test_adjust_request_language_handles_empty_request(self):
"""
Проверяем, что метод корректно обрабатывает пустой запрос
"""
request = self.factory.get('/')
request.method = 'GET'
request = Request(request)
# Метод не должен выбрасывать исключений
try:
LocaleManager.adjust_request_language(request)
except Exception as e:
self.fail(f"adjust_request_language raised {e}")