Исходный код EJIO.utils.system.localization

## /utils/system/localization.py

import json
import sys
from threading import Lock
from pathlib import Path
from typing import Any
from loguru import logger

from ..interface import MutableStaticMeta, StaticClass, Disposable
from .properties import SystemProperties
from .err import LocalizationError

__all__ = [
    'Localization',
    '_',
]


[документация] class Localization(StaticClass, Disposable, metaclass=MutableStaticMeta): """ Глобальный статический менеджер локализации (i18n). Хранит словари переводов и обеспечивает рантайм-доступ к фразам. """ __slots__ = () _LOCK: Lock = Lock() # Текущий активный язык (по умолчанию подтягивается из ядра ОС) # Обрезаем до двух символов (например, 'ru_RU' -> 'ru') current_locale: str = "en" # Глобальное дерево переводов: { 'ru': { 'auth': { 'title': 'Вход' } } } _translations: dict[str, dict[str, Any]] = { "en": {}, "ru": {} }
[документация] @classmethod def initialize(cls) -> None: """Автоматическая инициализация дефолтного системного языка.""" with cls._LOCK: sys_lang = SystemProperties.language.split("_")[0].lower() #type ignore[attr-defined] if sys_lang in cls._translations: cls.current_locale = sys_lang else: cls.current_locale = "en" logger.info(f"Localization initialized. Default system language: '{cls.current_locale}'")
[документация] @classmethod def load_from_dict(cls, locale: str, data: dict[str, Any]) -> None: """ Загружает словарь переводов. Безопасно выполняет глубокое рекурсивное слияние, если ключи (например, 'ERRORS') пересекаются в разных модулях. :param locale: Локаль, данные для которой нужно установить. :param data: Словарь с данными, которые необходимо загрузить. """ with cls._LOCK: if locale not in cls._translations: cls._translations[locale] = {} cls._deep_merge(cls._translations[locale], data) # --- ИНТРОСПЕКЦИЯ СТЕКА ВЫЗОВА ДЛЯ ИНФОРМАТИВНОГО ЛОГА --- try: # f_back возвращает кадр (frame) функции, которая вызвала load_from_dict caller_frame = sys._getframe(1) caller_module = caller_frame.f_globals.get('__name__', 'unknown_module') caller_file = Path(caller_frame.f_globals.get('__file__', '')).name # Используем .opt() логуру для динамической подмены контекста в логе logger.patch(lambda record: record.update( #type ignore[call-arg] name=caller_module, #type ignore[call-arg] file=caller_file #type ignore[call-arg] )).debug(f"Translations loaded for locale '{locale}' (Source: {caller_module})") except Exception: # Резервный вариант, если стек недоступен в данной сборке Python logger.debug(f"Translations loaded from dict for locale '{locale}'")
@classmethod def _deep_merge(cls, target: dict[str, Any], source: dict[str, Any]) -> None: """ Внутренний рекурсивный метод для слияния вложенных словарей. :param target: Словарь для записи новых значений. :param source: Словарь содержащий новые значения. """ for key, value in source.items(): if key in target and isinstance(target[key], dict) and isinstance(value, dict): # Если оба элемента являются словарями, уходим в рекурсию cls._deep_merge(target[key], value) else: # В противном случае (новый ключ или перезапись конечной строки) просто копируем target[key] = value
[документация] @classmethod def load_from_json(cls, locale: str, file_path: str | Path) -> None: """ Загружает переводы из внешнего JSON-файла. :param locale: Локаль, данные для которой нужно установить. :param file_path: Путь к файлу со словарем с данными, которые необходимо загрузить. """ try: path = Path(file_path) if not path.exists(): raise FileNotFoundError(f"Localization file {path} not found.") with open(path, "r", encoding="utf-8") as f: data = json.load(f) cls.load_from_dict(locale, data) except Exception as e: logger.error(f"Failed to load localization file for '{locale}': {e}")
[документация] @classmethod def set_locale(cls, locale: str) -> None: """ Переключает текущий активный язык приложения в рантайме. :param locale: Локаль которую необходимо установить. :raises LocalizationError: Если данная локаль не инициализированна. """ with cls._LOCK: if locale not in cls._translations: logger.error(f"Error switching to unregistered locale: '{locale}'") raise LocalizationError(f"Error switching to unregistered locale: '{locale}'") cls.current_locale = locale logger.info(f"Application language changed to: '{locale}'")
[документация] @classmethod def get(cls, key_path: str, default: str | None = None) -> str: """ Извлекает фразу по иерархическому пути через точку. Пример: Localization.get("auth.login_btn") :param key_path: Путь к ключу (например, 'main_menu.profile.title'). :param default: Значение по умолчанию, если ключ не найден. :return: Значение, установленное по данному ключу. """ with cls._LOCK: locale_dict = cls._translations.get(cls.current_locale, {}) # Резервный откат на английский, если в текущем языке вообще нет словаря if not locale_dict and cls.current_locale != "en": locale_dict = cls._translations.get("en", {}) # Идем вглубь словаря по точкам (разбиваем 'auth.btn' -> ['auth', 'btn']) keys = key_path.split(".") current_node: Any = locale_dict for key in keys: if isinstance(current_node, dict) and key in current_node: current_node = current_node[key] else: # Если ключ не найден, пробуем поискать в дефолтном английском if cls.current_locale != "en": return cls._get_fallback(key_path, default) return default or f"[{key_path}]" return str(current_node)
@classmethod def _get_fallback(cls, key_path: str, default: str | None = None) -> str: """ Внутренний резервный поиск фразы в английском словаре (fallback). :param key_path: Путь к ключу (например, 'main_menu.profile.title'). :param default: Значение по умолчанию, если ключ не найден. :return: Значение, установленное по данному ключу. """ en_dict = cls._translations.get("en", {}) keys = key_path.split(".") current_node: Any = en_dict for key in keys: if isinstance(current_node, dict) and key in current_node: current_node = current_node[key] else: return default or f"[{key_path}]" return str(current_node)
[документация] @classmethod def clear(cls) -> None: """Потокобезопасно очищает все загруженные переводы и сбрасывает локаль.""" with cls._LOCK: cls.current_locale = "en" cls._translations.clear() cls._translations["en"] = {} cls._translations["ru"] = {}
[документация] @classmethod def destroy(cls) -> None: """Потокобезопасно очищает все загруженные переводы и сбрасывает локаль.""" cls.clear()
def _(key_path: str, default: str | None = None) -> str: """ Глобальная функция-шорткат (общепринятый стандарт i18n). Позволяет лаконично вызывать перевод в любом месте кода: _("auth.title") :param key_path: Путь до нужной фразы. :param default: Значение по умолчанию, если в словаре фраза не зарегестрирована. :return: Фраза, зарегестрированая под ключом. """ return Localization.get(key_path, default)