gonets/SRV/utils/i18n.py

83 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Утилиты локализации и зависимости переводов для FastAPI.
Содержимое модуля:
- `get_translations()`: фабрика зависимости FastAPI, считывающая локаль из
query-параметра `locale`, cookie `locale` или заголовка `Accept-Language`
(по приоритету), с дефолтом `ru`. Возвращает функцию-зависимость, которая
подгружает `babel.support.Translations` из каталога `SRV/locales`.
- `_()`: удобная обёртка для получения перевода строки с использованием
текущей локали (или дефолтного `ru`, если контекст запроса не передан).
Особенности:
- При отсутствии словаря перевода для выбранной локали выполняется fallback на
русскую локаль, что гарантирует наличие строк интерфейса.
- Локаль определяется только по первому языку из `Accept-Language` (без
региональных суффиксов) для упрощения.
- Модуль ведёт отладочное логирование выбора и загрузки локали.
"""
from fastapi import Request, WebSocket
from babel.support import Translations
import logging
from typing import Optional, Union
from pathlib import Path
logger = logging.getLogger(__name__)
LOCALES_DIR = Path(__file__).resolve().parent.parent / "locales"
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")
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")
# Проверяем параметр запроса '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]
locale = locale or "ru"
logger.debug(f"Determined locale: {locale}")
return _load_translations(locale)
return translations_dependency
def _(msgid: str, request: Request = None) -> str:
"""
Переводит строку msgid для текущей локали.
Args:
msgid (str): Строка для перевода.
request (Request, optional): Объект запроса FastAPI для получения локали.
Returns:
str: Переведенная строка.
"""
if request is None:
# Язык, который будет отображаться по умолчанию
translations = _load_translations("ru")
logger.debug(f"Using default ru translations for msgid: {msgid}")
else:
translations = get_translations()(request)
logger.debug(f"Using translations for locale {request.cookies.get('locale', 'ru')} for msgid: {msgid}")
return translations.gettext(msgid)