repair translations
This commit is contained in:
parent
98581132dd
commit
84adc5d941
18 changed files with 304 additions and 122 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue