Добавление класса LocaleManager
This commit is contained in:
@@ -21,3 +21,19 @@ class UILanguage(CoreEnum):
|
||||
|
||||
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}")
|
||||
|
||||
11
net/xeaf/rack/managers/__init__.py
Normal file
11
net/xeaf/rack/managers/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет классов менеджеров
|
||||
"""
|
||||
|
||||
from .locale_manager import LocaleManager
|
||||
126
net/xeaf/rack/managers/locale_manager.py
Normal file
126
net/xeaf/rack/managers/locale_manager.py
Normal 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)
|
||||
@@ -13,7 +13,7 @@ from django.test import TestCase
|
||||
from net.xeaf.rack.enums import UILanguage
|
||||
|
||||
|
||||
class UILanguageTest(TestCase):
|
||||
class UILanguageTests(TestCase):
|
||||
"""
|
||||
Тесты для перечисления UILanguage
|
||||
"""
|
||||
@@ -34,3 +34,79 @@ class UILanguageTest(TestCase):
|
||||
for code in UILanguage.values():
|
||||
self.assertEqual(len(code), 2)
|
||||
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' не поддерживается
|
||||
|
||||
12
net/xeaf/rack/tests/managers/__init__.py
Normal file
12
net/xeaf/rack/tests/managers/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Пакет описания тестов для классов менеджеров
|
||||
"""
|
||||
|
||||
from .locale_manager_tests import LocaleManagerTests
|
||||
|
||||
245
net/xeaf/rack/tests/managers/locale_manager_tests.py
Normal file
245
net/xeaf/rack/tests/managers/locale_manager_tests.py
Normal 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}")
|
||||
Reference in New Issue
Block a user