Добавление класса LocaleManager
This commit is contained in:
@@ -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}")
|
||||||
|
|||||||
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
|
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' не поддерживается
|
||||||
|
|||||||
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