From 84adc5d941ae74a36d2344cbb1cb27f51838fd4d Mon Sep 17 00:00:00 2001 From: "s.galanskiy" Date: Wed, 19 Nov 2025 15:03:02 +0500 Subject: [PATCH] repair translations --- SRV/api/v1/auth.py | 12 +-- SRV/api/v1/ip.py | 8 +- SRV/api/v1/network.py | 17 ++-- SRV/api/v1/settings.py | 16 ++-- SRV/api/v1/subnet.py | 22 +++--- SRV/api/v1/websocket.py | 12 +-- SRV/logs/app.log | 130 +++++++++++++++++++++++++++++++ SRV/main.py | 18 +++++ SRV/models/network.py | 2 +- SRV/services/arp_service.py | 4 +- SRV/services/auth_service.py | 4 +- SRV/services/connect_service.py | 4 +- SRV/services/ip_service.py | 10 ++- SRV/services/network_service.py | 31 +++++--- SRV/services/settings_service.py | 4 +- SRV/services/subnet_service.py | 4 +- SRV/services/wifi_service.py | 8 +- SRV/utils/i18n.py | 120 ++++++++++++++++------------ 18 files changed, 304 insertions(+), 122 deletions(-) create mode 100644 SRV/logs/app.log diff --git a/SRV/api/v1/auth.py b/SRV/api/v1/auth.py index a6de017..7238ff3 100644 --- a/SRV/api/v1/auth.py +++ b/SRV/api/v1/auth.py @@ -16,7 +16,7 @@ auth_service = AuthService() oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") @router.post("/register", response_model=UserResponse) -async def register(user: UserCreate, translations: Translations = Depends(get_translations())): +async def register(user: UserCreate, translations: Translations = Depends(get_translations)): """Регистрирует нового пользователя. Создаёт нового пользователя на основе предоставленных данных и возвращает @@ -44,7 +44,7 @@ async def register(user: UserCreate, translations: Translations = Depends(get_tr raise HTTPException(status_code=500, detail=translations.gettext("Не удалось зарегистрировать пользователя")) @router.post("/login", response_model=Token) -async def login(user: UserLogin, request: Request, translations: Translations = Depends(get_translations())): +async def login(user: UserLogin, request: Request, translations: Translations = Depends(get_translations)): """Авторизует пользователя и создаёт сессию. Проверяет учетные данные пользователя и возвращает токен и session_id для @@ -75,7 +75,7 @@ async def login(user: UserLogin, request: Request, translations: Translations = raise HTTPException(status_code=500, detail=translations.gettext("Не удалось авторизовать пользователя")) @router.get("/me") -async def get_current_user(request: Request, translations: Translations = Depends(get_translations())): +async def get_current_user(request: Request, translations: Translations = Depends(get_translations)): """Возвращает данные текущего пользователя и токен. Проверяет активную сессию по client_id и возвращает информацию о пользователе @@ -109,7 +109,7 @@ async def get_current_user(request: Request, translations: Translations = Depend raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить данные пользователя")) @router.get("/logout") -async def logout(request: Request, translations: Translations = Depends(get_translations())): +async def logout(request: Request, translations: Translations = Depends(get_translations)): """Выход из системы, удаление сессии. Удаляет активную сессию пользователя по client_id. @@ -136,7 +136,7 @@ async def logout(request: Request, translations: Translations = Depends(get_tran raise HTTPException(status_code=500, detail=translations.gettext("Не удалось выйти из системы")) @router.get("/check_json") -async def check_users_file(translations: Translations = Depends(get_translations())): +async def check_users_file(translations: Translations = Depends(get_translations)): """Проверяет, существует ли файл users.json и содержит ли он пользователей. Возвращает информацию о состоянии файла и наличии пользователей. @@ -180,4 +180,4 @@ async def check_users_file(translations: Translations = Depends(get_translations } except Exception as e: logger.error(f"Ошибка при проверке файла: {type(e).__name__} - {str(e)}") - raise HTTPException(status_code=500, detail=translations.gettext("Ошибка при проверке файла")) \ No newline at end of file + raise HTTPException(status_code=500, detail=translations.gettext("Ошибка при проверке файла")) diff --git a/SRV/api/v1/ip.py b/SRV/api/v1/ip.py index efa116e..4fdefe0 100644 --- a/SRV/api/v1/ip.py +++ b/SRV/api/v1/ip.py @@ -8,7 +8,7 @@ router = APIRouter() ip_service = IPService() @router.post("/check_ip") -async def check_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations())): +async def check_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)): """Проверяет доступность IP-адреса перед его применением. Выполняет ARP-сканирование для проверки, занят ли указанный IP-адрес, @@ -37,7 +37,7 @@ async def check_ip_config(data: Dict[str, Any] = Body(...), translations: Transl ) @router.post("/apply_ip") -async def apply_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations())): +async def apply_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)): """Применяет проверенные IP-настройки к указанному сетевому профилю. Обновляет настройки соединения через NetworkManager, отключает и @@ -74,7 +74,7 @@ async def apply_ip_config(data: Dict[str, Any] = Body(...), translations: Transl ) @router.post("/save_ip") -async def save_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations())): +async def save_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)): """Применяет проверенные IP-настройки для отката сетевого профиля. Используется для сохранения и отката настроек сетевого профиля к предыдущему @@ -108,4 +108,4 @@ async def save_ip_config(data: Dict[str, Any] = Body(...), translations: Transla raise HTTPException( status_code=500, detail=translations.gettext("Ошибка проверки IP конфигурации: {error}").format(error=str(e)) - ) \ No newline at end of file + ) diff --git a/SRV/api/v1/network.py b/SRV/api/v1/network.py index c3e098f..395bc78 100644 --- a/SRV/api/v1/network.py +++ b/SRV/api/v1/network.py @@ -9,7 +9,7 @@ from models.network import ( NetworkInterface, WiFiDevice, ) -from utils.i18n import get_translations, _ +from utils.i18n import get_translations, _, _load_translations from babel.support import Translations logging.basicConfig(level=logging.INFO) @@ -44,7 +44,7 @@ class ContainerPositionManager: }, } - def get_position_and_state(self, container_name: str, translations: Translations = None) -> Tuple[int, int, bool]: + def get_position_and_state(self, container_name: str, translations: Translations) -> Tuple[int, int, bool]: """Получает текущие координаты и состояние указанного контейнера. Args: @@ -57,7 +57,6 @@ class ContainerPositionManager: Raises: ValueError: Если указан неверный идентификатор контейнера. """ - translations = translations or get_translations()() container = self.positions.get(container_name) if not container: raise ValueError(translations.gettext("Неверное имя контейнера: {container_name}").format(container_name=container_name)) @@ -80,7 +79,7 @@ class ContainerPositionManager: container_manager = ContainerPositionManager() -async def get_network_data(translations: Translations = None) -> Dict[str, Dict]: +async def get_network_data(translations: Translations = Depends(get_translations)) -> Dict[str, Dict]: """Возвращает текущие сетевые данные в виде словаря. Получает данные о сетевых интерфейсах, разделяет их на активные и неактивные, @@ -97,15 +96,14 @@ async def get_network_data(translations: Translations = None) -> Dict[str, Dict] Raises: HTTPException: Если произошла ошибка при получении данных (status_code=500). """ - translations = translations or get_translations()() logger.debug("Calling get_network_data()") try: network_data = await network_service.get_network_interfaces_container() activation_interfaces, deactivation_interfaces = _split_interfaces(network_data.interfaces) - activation_x, activation_y, activation_is_open = (container_manager.get_position_and_state("activation")) - deactivation_x, deactivation_y, deactivation_is_open = (container_manager.get_position_and_state("deactivation")) + activation_x, activation_y, activation_is_open = container_manager.get_position_and_state("activation", translations) + deactivation_x, deactivation_y, deactivation_is_open = container_manager.get_position_and_state("deactivation", translations) activation = Activation( id=1, @@ -152,7 +150,7 @@ def _split_interfaces( return activation_interfaces, deactivation_interfaces @router.post("/update_container_position") -async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...), translations: Translations = None): +async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...), translations: Translations = Depends(get_translations)): """Обновляет позицию и состояние контейнера. Принимает данные о новом положении и состоянии контейнера и обновляет их @@ -175,7 +173,6 @@ async def update_container_position(data: Dict[str, Union[int, bool]] = Body(... HTTPException: Если отсутствуют обязательные поля (status_code=400) или типы данных неверны. """ - translations = translations or get_translations()() try: required_keys = [ "container_id", "container_x", "container_y", "isOpen" @@ -206,7 +203,7 @@ async def update_container_position(data: Dict[str, Union[int, bool]] = Body(... raise HTTPException(status_code=400, detail=translations.gettext("Не удалось обновить положение контейнера: {error}").format(error=str(e))) @router.get("/get_hostname") -async def get_hostname(translations: Translations = Depends(get_translations())): +async def get_hostname(translations: Translations = Depends(get_translations)): """Возвращает имя хоста системы. Выполняет запрос к сервису для получения имени хоста. diff --git a/SRV/api/v1/settings.py b/SRV/api/v1/settings.py index 63f6b36..27773bc 100644 --- a/SRV/api/v1/settings.py +++ b/SRV/api/v1/settings.py @@ -16,7 +16,7 @@ router = APIRouter() settings_service = SettingsService() @router.post("/clear_subnet") -def clear_subnet(subnet: Optional[str] = None, translations: Translations = Depends(get_translations())): +def clear_subnet(subnet: Optional[str] = None, translations: Translations = Depends(get_translations)): """Очищает данные узлов подсети, сохранённые в devices.json. Удаляет все записи об узлах подсети, оставляя файл пустым. @@ -46,7 +46,7 @@ def clear_subnet(subnet: Optional[str] = None, translations: Translations = Depe raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e))) @router.post("/clear_wifi") -def clear_wifi(translations: Translations = Depends(get_translations())): +def clear_wifi(translations: Translations = Depends(get_translations)): """Очищает данные точек доступа Wi-Fi, сохранённые в wifi.json. Удаляет все записи о точках доступа, оставляя файл пустым. @@ -71,7 +71,7 @@ def clear_wifi(translations: Translations = Depends(get_translations())): raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e))) @router.post("/clear_all_data") -def clear_all_data(translations: Translations = Depends(get_translations())): +def clear_all_data(translations: Translations = Depends(get_translations)): """Очищает данные узлов подсети и точек доступа Wi-Fi в обоих файлах. Удаляет все записи из devices.json и wifi.json. @@ -98,7 +98,7 @@ def clear_all_data(translations: Translations = Depends(get_translations())): WIFI_PATH = "storage/wifi/wifi.json" @router.post("/export_devices", response_class=FileResponse) -async def export_devices(request: SubnetScanRequest = Body(...), translations: Translations = Depends(get_translations())): +async def export_devices(request: SubnetScanRequest = Body(...), translations: Translations = Depends(get_translations)): """ Экспортирует файл с данными устройств в формате JSON для указанной подсети. @@ -125,7 +125,7 @@ async def export_devices(request: SubnetScanRequest = Body(...), translations: T ) @router.get("/export_wifi", response_class=FileResponse) -async def export_wifi(translations: Translations = Depends(get_translations())): +async def export_wifi(translations: Translations = Depends(get_translations)): """ Экспортирует файл с данными устройств в формате JSON. @@ -149,7 +149,7 @@ async def export_wifi(translations: Translations = Depends(get_translations())): ) @router.post("/set_language") -def set_language(locale: str, response: Response, translations: Translations = Depends(get_translations())): +def set_language(locale: str, response: Response, translations: Translations = Depends(get_translations)): """ Устанавливает куку locale для смены языка пользователя. @@ -177,7 +177,7 @@ def set_language(locale: str, response: Response, translations: Translations = D return {"message": translations.gettext("Язык успешно установлен: {locale}").format(locale=locale)} @router.get("/get_system_language") -async def get_system_language(request: Request, translations: Translations = Depends(get_translations())): +async def get_system_language(request: Request, translations: Translations = Depends(get_translations)): """Выводит текущий язык, установленный в системе на основе locale. Выполняет команду 'locale' в системе и извлекает параметры LANG @@ -206,4 +206,4 @@ async def get_system_language(request: Request, translations: Translations = Dep raise HTTPException(status_code=500, detail=translations.gettext("Не удалось определить язык в системе")) except Exception as e: logger.error(f"Unexpected error getting system language: {str(e)}") - raise HTTPException(status_code=500, detail=translations.gettext("Ошибка при определении языка в системе {error}").format(error=str(e))) \ No newline at end of file + raise HTTPException(status_code=500, detail=translations.gettext("Ошибка при определении языка в системе {error}").format(error=str(e))) diff --git a/SRV/api/v1/subnet.py b/SRV/api/v1/subnet.py index 696fb52..0116d2a 100644 --- a/SRV/api/v1/subnet.py +++ b/SRV/api/v1/subnet.py @@ -15,7 +15,7 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @router.post("/devices", response_model=SubnetScanResult) -def scan_subnet(request: SubnetScanRequest = Body(...), subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations())): +def scan_subnet(request: SubnetScanRequest = Body(...), subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)): """Сканирует локальную подсеть и возвращает данные об устройствах. Выполняет сканирование указанной подсети для обнаружения активных устройств. @@ -44,7 +44,7 @@ def scan_subnet(request: SubnetScanRequest = Body(...), subnet_service: SubnetSe raise HTTPException(status_code=500, detail=translations.gettext("Ошибка сканирования подсети: {error}").format(error=str(e))) @router.get("/wifi", response_model=WifiScanResult) -def scan_wifi(timeout: int = 30, translations: Translations = Depends(get_translations())): +def scan_wifi(timeout: int = 30, translations: Translations = Depends(get_translations)): """Сканирует Wi-Fi точки доступа и возвращает данные о них. Выполняет сканирование доступных Wi-Fi сетей с указанным таймаутом. @@ -67,7 +67,7 @@ def scan_wifi(timeout: int = 30, translations: Translations = Depends(get_transl raise HTTPException(status_code=500, detail=translations.gettext("Ошибка сканирования Wi-Fi: {error}").format(error=str(e))) @router.post("/devices/{mac}/note") -def add_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations())): +def add_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)): """Добавляет заметку для устройства по MAC-адресу. Привязывает заметку к устройству, идентифицируемому по MAC-адресу. @@ -101,7 +101,7 @@ def add_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Dep raise HTTPException(status_code=500, detail=translations.gettext("Ошибка добавления заметки: {error}").format(error=str(e))) @router.put("/devices/{mac}/note") -def update_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations())): +def update_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)): """Обновляет заметку для устройства по MAC-адресу. Изменяет существующую заметку, привязанную к устройству. @@ -135,7 +135,7 @@ def update_note(mac: str, request: NoteRequest, subnet_service: SubnetService = raise HTTPException(status_code=500, detail=translations.gettext("Ошибка обновления заметки: {error}").format(error=str(e))) @router.delete("/devices/{mac}/note") -def delete_note(mac: str, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations())): +def delete_note(mac: str, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)): """Удаляет заметку для устройства по MAC-адресу. Удаляет привязанную заметку для указанного устройства. @@ -168,7 +168,7 @@ def delete_note(mac: str, subnet_service: SubnetService = Depends(get_subnet_ser raise HTTPException(status_code=500, detail=translations.gettext("Ошибка удаления заметки: {error}").format(error=str(e))) @router.post("/wifi/{ssid}/note") -def add_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations())): +def add_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations)): """Добавляет заметку для сети Wi-Fi по SSID. Привязывает заметку к Wi-Fi сети, идентифицируемой по SSID. @@ -202,7 +202,7 @@ def add_wifi_note(ssid: str, request: NoteRequest, translations: Translations = raise HTTPException(status_code=500, detail=translations.gettext("Ошибка добавления заметки: {error}").format(error=str(e))) @router.put("/wifi/{ssid}/note") -def update_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations())): +def update_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations)): """Обновляет заметку для сети Wi-Fi по SSID. Изменяет существующую заметку, привязанную к Wi-Fi сети. @@ -236,7 +236,7 @@ def update_wifi_note(ssid: str, request: NoteRequest, translations: Translations raise HTTPException(status_code=500, detail=translations.gettext("Ошибка обновления заметки: {error}").format(error=str(e))) @router.delete("/wifi/{ssid}/note") -def delete_wifi_note(ssid: str, translations: Translations = Depends(get_translations())): +def delete_wifi_note(ssid: str, translations: Translations = Depends(get_translations)): """Удаляет заметку для сети Wi-Fi по SSID. Удаляет привязанную заметку для указанной Wi-Fi сети. @@ -269,7 +269,7 @@ def delete_wifi_note(ssid: str, translations: Translations = Depends(get_transla raise HTTPException(status_code=500, detail=translations.gettext("Ошибка удаления заметки: {error}").format(error=str(e))) @router.post("/wifi/connect") -def connect_to_wifi(request: WifiConnectRequest, translations: Translations = Depends(get_translations())): +def connect_to_wifi(request: WifiConnectRequest, translations: Translations = Depends(get_translations)): """Подключается к Wi-Fi сети по SSID и паролю. Выполняет подключение к указанной Wi-Fi сети с использованием предоставленных @@ -297,7 +297,7 @@ def connect_to_wifi(request: WifiConnectRequest, translations: Translations = De raise HTTPException(status_code=500, detail=translations.gettext("Ошибка подключения к Wi-Fi: {error}").format(error=str(e))) @router.post("/wifi/disconnect") -def disconnect_from_wifi(request: WifiDisconnectRequest, translations: Translations = Depends(get_translations())): +def disconnect_from_wifi(request: WifiDisconnectRequest, translations: Translations = Depends(get_translations)): """Отключается от Wi-Fi сети по указанному SSID. Выполняет отключение от указанной Wi-Fi сети. @@ -324,7 +324,7 @@ def disconnect_from_wifi(request: WifiDisconnectRequest, translations: Translati raise HTTPException(status_code=500, detail=translations.gettext("Ошибка отключения от Wi-Fi: {error}").format(error=str(e))) @router.get("/wifi/status") -def get_wifi_status(translations: Translations = Depends(get_translations())): +def get_wifi_status(translations: Translations = Depends(get_translations)): """Проверяет статус текущего подключения к Wi-Fi. Возвращает информацию о текущем состоянии подключения и SSID, если подключение активно. diff --git a/SRV/api/v1/websocket.py b/SRV/api/v1/websocket.py index 0dc9994..d122645 100644 --- a/SRV/api/v1/websocket.py +++ b/SRV/api/v1/websocket.py @@ -4,7 +4,7 @@ from typing import Set from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends from starlette.websockets import WebSocketState -from utils.i18n import get_translations, _ +from utils.i18n import get_translations_ws, _, _load_translations from babel.support import Translations from .network import update_container_position, get_network_data @@ -78,7 +78,7 @@ class WebSocketManager: manager = WebSocketManager() @router.websocket("/api/v1") -async def websocket_endpoint(websocket: WebSocket, translations: Translations = Depends(get_translations())): +async def websocket_endpoint(websocket: WebSocket, translations: Translations = Depends(get_translations_ws)): """Обработчик WebSocket-соединения. Управляет соединением клиента, принимает данные для обновления позиций @@ -99,10 +99,10 @@ async def websocket_endpoint(websocket: WebSocket, translations: Translations = logger.info(f"Received data: {data}") if all(key in data for key in ["container_id", "container_x", "container_y"]): - await update_container_position(data) + await update_container_position(data, translations=translations) - # Внутренний вызов без DI, поэтому используем fallback-переводы - network_data = await get_network_data(translations=get_translations()(websocket)) + # Для внутреннего вызова используем переводы из WebSocket зависимостии + network_data = await get_network_data(translations=translations) logger.info(f"Current network data: {network_data}") await manager.broadcast(network_data) @@ -139,7 +139,7 @@ async def broadcast_data(translations: Translations = None): Raises: Exception: Если произошла ошибка при получении данных или трансляции. """ - translations = translations or get_translations()() + translations = translations or _load_translations("ru") while True: try: network_data = await get_network_data(translations=translations) diff --git a/SRV/logs/app.log b/SRV/logs/app.log new file mode 100644 index 0000000..86daed1 --- /dev/null +++ b/SRV/logs/app.log @@ -0,0 +1,130 @@ +2025-11-17 12:33:54,542 - watchfiles.main - INFO - 4 changes detected +2025-11-17 12:33:54,553 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined +2025-11-17 12:33:54,553 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:33:54,553 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined +2025-11-17 12:33:54,554 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:33:54,554 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined +2025-11-17 12:33:54,554 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:33:54,894 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:33:55,245 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:33:55,595 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:33:55,946 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:33:56,297 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:33:56,594 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined +2025-11-17 12:33:56,594 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:33:56,594 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined +2025-11-17 12:33:56,594 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:33:56,595 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined +2025-11-17 12:33:56,595 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:33:56,648 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:33:56,999 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:33:57,350 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:33:57,701 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:33:58,052 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:33:58,403 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:33:58,638 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined +2025-11-17 12:33:58,638 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:33:58,639 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined +2025-11-17 12:33:58,639 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:33:58,639 - api.v1.network - ERROR - Error in get_network_data: name '_load_translations' is not defined +2025-11-17 12:33:58,639 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:33:58,754 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:33:59,105 - watchfiles.main - INFO - 2 changes detected +2025-11-17 12:33:59,571 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:33:59,922 - watchfiles.main - INFO - 4 changes detected +2025-11-17 12:34:00,273 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:34:00,624 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:34:00,975 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:34:01,326 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:34:01,677 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:34:02,028 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:34:02,379 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:34:02,730 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:34:03,081 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:34:03,432 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:34:03,782 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:34:04,133 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:34:04,484 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:34:04,835 - watchfiles.main - INFO - 1 change detected +2025-11-17 12:55:17,396 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice +mode.str + Input should be a valid string [type=string_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/string_type +mode.int + Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/int_type +2025-11-17 12:55:17,397 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:55:17,397 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice +mode.str + Input should be a valid string [type=string_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/string_type +mode.int + Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/int_type +2025-11-17 12:55:17,397 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:55:17,397 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice +mode.str + Input should be a valid string [type=string_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/string_type +mode.int + Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/int_type +2025-11-17 12:55:17,397 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:55:19,407 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice +mode.str + Input should be a valid string [type=string_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/string_type +mode.int + Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/int_type +2025-11-17 12:55:19,408 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:55:19,408 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice +mode.str + Input should be a valid string [type=string_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/string_type +mode.int + Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/int_type +2025-11-17 12:55:19,408 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:55:19,408 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice +mode.str + Input should be a valid string [type=string_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/string_type +mode.int + Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/int_type +2025-11-17 12:55:19,408 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:55:39,690 - api.v1.network - ERROR - Error in get_network_data: The name org.freedesktop.NetworkManager was not provided by any .service files +2025-11-17 12:55:39,690 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:55:39,691 - api.v1.network - ERROR - Error in get_network_data: The name org.freedesktop.NetworkManager was not provided by any .service files +2025-11-17 12:55:39,691 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:55:39,691 - api.v1.network - ERROR - Error in get_network_data: The name org.freedesktop.NetworkManager was not provided by any .service files +2025-11-17 12:55:39,691 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:55:41,721 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice +mode.str + Input should be a valid string [type=string_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/string_type +mode.int + Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/int_type +2025-11-17 12:55:41,721 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:55:41,721 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice +mode.str + Input should be a valid string [type=string_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/string_type +mode.int + Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/int_type +2025-11-17 12:55:41,721 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 12:55:41,721 - api.v1.network - ERROR - Error in get_network_data: 2 validation errors for WiFiDevice +mode.str + Input should be a valid string [type=string_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/string_type +mode.int + Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType] + For further information visit https://errors.pydantic.dev/2.12/v/int_type +2025-11-17 12:55:41,721 - api.v1.websocket - ERROR - Error in broadcast_data: 500: Не удалось получить сетевые данные +2025-11-17 17:45:00,050 - asyncio - ERROR - Task was destroyed but it is pending! +task: wait_for=> +2025-11-17 17:45:00,050 - asyncio - ERROR - Task was destroyed but it is pending! +task: wait_for=> diff --git a/SRV/main.py b/SRV/main.py index 3f0e419..a73e385 100644 --- a/SRV/main.py +++ b/SRV/main.py @@ -28,6 +28,24 @@ import uvicorn from api.router import router from config.cors import CORSMiddlewareConfig from services.network_service import NetworkService +import logging +from pathlib import Path + +# Логируем в файл logs/app.log и одновременно в stdout +LOG_DIR = Path(__file__).resolve().parent / "logs" +LOG_DIR.mkdir(parents=True, exist_ok=True) +LOG_FILE = LOG_DIR / "app.log" +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[ + logging.StreamHandler(), + logging.FileHandler(LOG_FILE, encoding="utf-8"), + ], + force=True, # переопределяем базовую конфигурацию, заданную в импортируемых модулях +) +# Глушим шум от hot-reload watchdog +logging.getLogger("watchfiles").setLevel(logging.WARNING) app = FastAPI( title="GONETS", diff --git a/SRV/models/network.py b/SRV/models/network.py index 8588842..bebbb81 100644 --- a/SRV/models/network.py +++ b/SRV/models/network.py @@ -113,7 +113,7 @@ class WiFiDevice(NetworkInterface): bitrate: Optional[int] = None strength: Optional[int] = None frequency: Optional[float] = None - mode: Union[str, int] = None + mode: Optional[Union[str, int]] = None class NetworkInterfacesContainer(BaseModel): diff --git a/SRV/services/arp_service.py b/SRV/services/arp_service.py index 4876696..9c18e20 100644 --- a/SRV/services/arp_service.py +++ b/SRV/services/arp_service.py @@ -20,7 +20,7 @@ import os import logging import netifaces from netaddr import IPNetwork -from utils.i18n import get_translations, _ +from utils.i18n import _, _load_translations from babel.support import Translations logger = logging.getLogger(__name__) @@ -54,7 +54,7 @@ class NetworkScanner: Инициализирует интерфейс, информацию об IP-адресах и локальный MAC-адрес. """ - self.translations = translations or get_translations()() + self.translations = translations or _load_translations("ru") self.interface = None self.ip_info = [] self.networks = [] diff --git a/SRV/services/auth_service.py b/SRV/services/auth_service.py index 3adbca2..7065755 100644 --- a/SRV/services/auth_service.py +++ b/SRV/services/auth_service.py @@ -9,7 +9,7 @@ from typing import Optional import configparser import os import logging -from utils.i18n import get_translations, _ +from utils.i18n import _, _load_translations from babel.support import Translations logging.basicConfig(level=logging.INFO) @@ -38,7 +38,7 @@ class AuthService: def __init__(self, translations: Optional[Translations] = None): self.storage = JsonStorage() self.session_storage = SessionStorage() - self.translations = translations or get_translations()() + self.translations = translations or _load_translations("ru") def create_access_token(self, data: dict) -> str: """Создаёт токен""" diff --git a/SRV/services/connect_service.py b/SRV/services/connect_service.py index 25de871..aef409f 100644 --- a/SRV/services/connect_service.py +++ b/SRV/services/connect_service.py @@ -18,7 +18,7 @@ import subprocess import logging from typing import Tuple, Optional -from utils.i18n import get_translations, _ +from utils.i18n import _load_translations, _ from babel.support import Translations logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") @@ -36,7 +36,7 @@ class WifiConnectionService: Автоматически определяет доступный Wi-Fi-интерфейс и сохраняет его имя. """ self.interface = self._get_wifi_interface() - self.translations = translations or get_translations()() + self.translations = translations or _load_translations("ru") def _get_wifi_interface(self) -> str: """Автоматически определяет Wi-Fi-интерфейс. diff --git a/SRV/services/ip_service.py b/SRV/services/ip_service.py index f552560..d48f3f4 100644 --- a/SRV/services/ip_service.py +++ b/SRV/services/ip_service.py @@ -21,7 +21,7 @@ from sdbus_async.networkmanager import ( import sdbus from sdbus_async.networkmanager.settings import ConnectionProfile from services.arp_service import NetworkScanner -from utils.i18n import get_translations +from utils.i18n import _load_translations from babel.support import Translations from fastapi import Depends @@ -54,7 +54,7 @@ class IPService: self.bus = None self.bus = new_bus - async def check_ip_config(self, args: Dict[str, Any], translations: Translations = Depends(get_translations())) -> Dict[str, Any]: + async def check_ip_config(self, args: Dict[str, Any], translations: Translations = None) -> Dict[str, Any]: """Проверяет доступность IP-адреса перед его применением. Выполняет ARP-сканирование сети для проверки, занят ли указанный IP-адрес. @@ -73,6 +73,7 @@ class IPService: Raises: Exception: Если произошла ошибка при сканировании ARP. """ + translations = translations or _load_translations("ru") self._reset_bus() clients = self.arp_scanner.scan_network() new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"] @@ -86,7 +87,7 @@ class IPService: "message": translations.gettext("Настройки сетевого профиля доступны для использования.") } - async def apply_ip_config(self, args: Dict[str, Any], translations: Translations = Depends(get_translations())) -> None: + async def apply_ip_config(self, args: Dict[str, Any], translations: Translations = None) -> None: """Применяет проверенные настройки IP к указанному соединению. Перезапускает шину D-Bus, обновляет настройки соединения через NetworkManager, @@ -105,6 +106,7 @@ class IPService: Note: Требуются права для изменения настроек через NetworkManager. """ + translations = translations or _load_translations("ru") self._reset_bus() nm = NetworkManager() settings = NetworkManagerSettings() @@ -140,4 +142,4 @@ class IPService: print(f"Reconnected to {args['connection_id']}") return - raise RuntimeError(translations.gettext("Устройство для интерфейса {interface} не найдено").format(interface=interface_name)) \ No newline at end of file + raise RuntimeError(translations.gettext("Устройство для интерфейса {interface} не найдено").format(interface=interface_name)) diff --git a/SRV/services/network_service.py b/SRV/services/network_service.py index 011f372..edf0cc1 100644 --- a/SRV/services/network_service.py +++ b/SRV/services/network_service.py @@ -32,9 +32,20 @@ from models.network import ( WiFiDevice, ) import subprocess -from utils.i18n import get_translations, _ +from utils.i18n import _, _load_translations from babel.support import Translations +def _extract_value(value): + """Приводит значения D-Bus к примитивам Python.""" + if isinstance(value, tuple) and len(value) == 2: + value = value[1] + if isinstance(value, bytes): + try: + return value.decode("utf-8") + except Exception: + return value.decode("utf-8", errors="ignore") + return value + class NetworkService: """Сервис для взаимодействия с NetworkManager через D-Bus. @@ -51,7 +62,7 @@ class NetworkService: self.system_bus = sd_bus_open_system() self.nm = NetworkManager(self.system_bus) self.nm_settings = NetworkManagerSettings(self.system_bus) - self.translations = translations or get_translations()() + self.translations = translations or _load_translations("ru") async def get_ip_method(self, device: NetworkDeviceGeneric, ip_type: str) -> str: """Получает метод IP (например, "auto", "manual") для указанного устройства. @@ -152,7 +163,7 @@ class NetworkService: wifi_info = { "ssid": None, "bssid": None, - "bitrate": await wifi_device.bitrate, + "bitrate": _extract_value(await wifi_device.bitrate), "strength": None, "frequency": None, "mode": None, @@ -160,11 +171,11 @@ class NetworkService: if active_ap != "/": ap = AccessPoint(active_ap, self.system_bus) wifi_info.update({ - "ssid": await ap.ssid, - "bssid": await ap.hw_address, - "strength": await ap.strength, - "frequency": await ap.frequency, - "mode": await ap.mode, + "ssid": _extract_value(await ap.ssid), + "bssid": _extract_value(await ap.hw_address), + "strength": _extract_value(await ap.strength), + "frequency": _extract_value(await ap.frequency), + "mode": _extract_value(await ap.mode), }) return wifi_info @@ -209,8 +220,8 @@ class NetworkService: "device_mtu": str(await generic_device.mtu), "ipv4": IPConfig(configurations=await self.get_ip_config(generic_device, "4")), "ipv6": IPConfig(configurations=await self.get_ip_config(generic_device, "6")), - "x": initial_positions.get(interface_name, (100, 100))[0], - "y": initial_positions.get(interface_name, (100, 100))[1], + "container_x": initial_positions.get(interface_name, (100, 100))[0], + "container_y": initial_positions.get(interface_name, (100, 100))[1], } if device_type == "WIFI": diff --git a/SRV/services/settings_service.py b/SRV/services/settings_service.py index 1703c93..f5ac7d0 100644 --- a/SRV/services/settings_service.py +++ b/SRV/services/settings_service.py @@ -2,7 +2,7 @@ import json import logging from typing import Optional -from utils.i18n import get_translations +from utils.i18n import _load_translations from babel.support import Translations from pathlib import Path @@ -15,7 +15,7 @@ class SettingsService: def __init__(self, translations: Translations = None): self.subnet_base_path = "storage/subnets" self.wifi_file = "storage/wifi/wifi.json" - self.translations = translations or get_translations()() + self.translations = translations or _load_translations("ru") def clear_subnet(self, subnet: Optional[str] = None) -> bool: """Очищает данные узлов подсети в указанном файле или во всех файлах. diff --git a/SRV/services/subnet_service.py b/SRV/services/subnet_service.py index 0b9ac09..ba991e1 100644 --- a/SRV/services/subnet_service.py +++ b/SRV/services/subnet_service.py @@ -27,7 +27,7 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) -def get_subnet_service(translations: Translations = Depends(get_translations())): +def get_subnet_service(translations: Translations = Depends(get_translations)): """Фабричная функция для создания экземпляра SubnetService. Использует зависимости FastAPI для получения объекта переводов и возвращает @@ -724,4 +724,4 @@ class SubnetService: return True, ("success", {"mac": mac}) except Exception as e: logger.error(f"Error deleting note for MAC {mac}: {str(e)}") - return False, ("internal_error", {"error": str(e)}) \ No newline at end of file + return False, ("internal_error", {"error": str(e)}) diff --git a/SRV/services/wifi_service.py b/SRV/services/wifi_service.py index 20f1dab..d5760b7 100644 --- a/SRV/services/wifi_service.py +++ b/SRV/services/wifi_service.py @@ -9,7 +9,7 @@ import pywifi from models.wifi import WifiAccessPoint, WifiScanResult from pathlib import Path import json -from utils.i18n import get_translations +from utils.i18n import _load_translations from babel.support import Translations # Настройка логирования @@ -36,7 +36,7 @@ class WifiStorage: def __init__(self, storage_path: str = "storage/wifi/wifi.json", translations: Translations = None): self.storage_file = Path(storage_path) self.storage_file.parent.mkdir(parents=True, exist_ok=True) - self.translations = translations or get_translations()() + self.translations = translations or _load_translations("ru") logger.info(f"Wi-Fi storage initialized at: {self.storage_file.absolute()}") def load_notes(self) -> Dict[str, Optional[str]]: @@ -88,10 +88,10 @@ class WifiService: self.mac_lookup = MacLookup() self.wifi = pywifi.PyWiFi() self.interface = self._get_wifi_interface() - validated_translations = translations or get_translations()() + validated_translations = translations or _load_translations("ru") if not hasattr(validated_translations, "gettext"): logger.warning("Translations object does not support gettext, using fallback") - validated_translations = get_translations()() + validated_translations = _load_translations("ru") self.translations = validated_translations self.storage = WifiStorage() diff --git a/SRV/utils/i18n.py b/SRV/utils/i18n.py index a15b3e4..847fd50 100644 --- a/SRV/utils/i18n.py +++ b/SRV/utils/i18n.py @@ -2,65 +2,90 @@ Утилиты локализации и зависимости переводов для FastAPI. Содержимое модуля: - - `get_translations()`: фабрика зависимости FastAPI, считывающая локаль из - query-параметра `locale`, cookie `locale` или заголовка `Accept-Language` - (по приоритету), с дефолтом `ru`. Возвращает функцию-зависимость, которая - подгружает `babel.support.Translations` из каталога `SRV/locales`. - - `_()`: удобная обёртка для получения перевода строки с использованием - текущей локали (или дефолтного `ru`, если контекст запроса не передан). - -Особенности: - - При отсутствии словаря перевода для выбранной локали выполняется fallback на - русскую локаль, что гарантирует наличие строк интерфейса. - - Локаль определяется только по первому языку из `Accept-Language` (без - региональных суффиксов) для упрощения. - - Модуль ведёт отладочное логирование выбора и загрузки локали. + - `get_translations`: зависимость FastAPI, считывающая локаль из query/cookie/header. + - `get_translations_ws`: аналог для WebSocket. + - `_load_translations`: низкоуровневая загрузка словаря переводов с fallback. + - `_()`: обёртка для gettext с дефолтом ru. """ from fastapi import Request, WebSocket from babel.support import Translations import logging -from typing import Optional, Union from pathlib import Path +import os +from typing import Iterable, Optional logger = logging.getLogger(__name__) -LOCALES_DIR = Path(__file__).resolve().parent.parent / "locales" + +def _candidate_locale_dirs() -> Iterable[Path]: + """Возвращает возможные каталоги с переводами.""" + configured = os.getenv("GONETS_LOCALES_PATH") + if configured: + configured_path = Path(configured).expanduser() + if configured_path.exists(): + yield configured_path + srv_root = Path(__file__).resolve().parent.parent + for candidate in ( + srv_root / "locales", + srv_root.parent / "locales", # на случай, если каталоги вынесены на уровень выше + Path("/usr/share/gonets/locales"), + Path("/usr/local/share/gonets/locales"), + ): + if candidate.exists(): + yield candidate + +LOCALE_DIRS = list(dict.fromkeys(_candidate_locale_dirs())) +if not LOCALE_DIRS: + logger.warning("Locales directory not found; translations will use NullTranslations") def _load_translations(locale: str) -> Translations: """Подгружает словарь переводов с fallback на ru.""" - try: - translations = Translations.load(str(LOCALES_DIR), [locale], "messages") - if not translations._catalog: - logger.warning(f"Translations for locale {locale} not found, falling back to ru") - translations = Translations.load(str(LOCALES_DIR), ["ru"], "messages") - return translations - except Exception as e: # pragma: no cover - логируем и возвращаем ru - logger.error(f"Failed to load translations for locale {locale}: {str(e)}") - return Translations.load(str(LOCALES_DIR), ["ru"], "messages") + last_error: Optional[str] = None + for directory in LOCALE_DIRS or [None]: + try: + if directory is None: + raise FileNotFoundError("Locales directory list is empty") + translations = Translations.load(str(directory), [locale], "messages") + if translations._catalog: + return translations + logger.warning( + "Translations catalog empty for locale %s in %s; trying fallback ru", + locale, + directory, + ) + ru_translations = Translations.load(str(directory), ["ru"], "messages") + if ru_translations._catalog: + return ru_translations + except Exception as exc: # pragma: no cover - логируем и возвращаем ru + last_error = str(exc) + continue + logger.error( + "Failed to load translations for locale %s from dirs %s: %s. Using NullTranslations", + locale, + LOCALE_DIRS, + last_error, + ) + return Translations() -def get_translations() -> callable: - """ - Создаёт зависимость для загрузки переводов на основе заголовка Accept-Language - или параметра запроса 'locale'. По умолчанию используется русский язык. - """ - def translations_dependency(request: Optional[Union[Request, WebSocket]] = None) -> Translations: - if request is None: - logger.warning("Translations dependency called without request/websocket; fallback to ru") - return _load_translations("ru") +def _resolve_locale_from_request(request: Request) -> str: + locale = request.query_params.get("locale") + if not locale: + locale = request.cookies.get("locale") + if not locale: + locale = request.headers.get("Accept-Language", "ru").split(",")[0].split("-")[0] + return locale or "ru" - # Проверяем параметр запроса 'locale', если он есть - locale = None - if hasattr(request, "query_params"): - locale = request.query_params.get("locale") - if not locale and hasattr(request, "cookies"): - locale = request.cookies.get("locale") - if not locale and hasattr(request, "headers"): - locale = request.headers.get("Accept-Language", "ru").split(",")[0].split("-")[0] +def get_translations(request: Request) -> Translations: + """Зависимость FastAPI для HTTP-эндпоинтов.""" + locale = _resolve_locale_from_request(request) + logger.debug(f"Determined locale (http): {locale}") + return _load_translations(locale) - locale = locale or "ru" - logger.debug(f"Determined locale: {locale}") - return _load_translations(locale) - - return translations_dependency +def get_translations_ws(websocket: WebSocket) -> Translations: + """Зависимость FastAPI для WebSocket-эндпоинтов.""" + # WebSocket не имеет cookies/headers в виде HTTPRequest, но есть headers + header_locale = websocket.headers.get("accept-language", "ru").split(",")[0].split("-")[0] if websocket.headers else "ru" + logger.debug(f"Determined locale (ws): {header_locale}") + return _load_translations(header_locale or "ru") def _(msgid: str, request: Request = None) -> str: """ @@ -74,10 +99,9 @@ def _(msgid: str, request: Request = None) -> str: str: Переведенная строка. """ if request is None: - # Язык, который будет отображаться по умолчанию translations = _load_translations("ru") logger.debug(f"Using default ru translations for msgid: {msgid}") else: - translations = get_translations()(request) + translations = get_translations(request) logger.debug(f"Using translations for locale {request.cookies.get('locale', 'ru')} for msgid: {msgid}") return translations.gettext(msgid)