"""Shared translation cache utilities.""" from __future__ import annotations import hashlib import logging from typing import Mapping, Optional try: import redis except ImportError: # pragma: no cover - runtime fallback for minimal envs redis = None # type: ignore[assignment] from config.loader import get_app_config logger = logging.getLogger(__name__) class TranslationCache: """Redis-backed cache shared by all translation capabilities.""" def __init__(self, config: Mapping[str, object]) -> None: self.ttl_seconds = int(config["ttl_seconds"]) self.sliding_expiration = bool(config["sliding_expiration"]) self.redis_client = self._init_redis_client() @property def available(self) -> bool: return self.redis_client is not None def build_key(self, *, model: str, target_lang: str, source_text: str) -> str: normalized_model = str(model or "").strip().lower() normalized_target_lang = str(target_lang or "").strip().lower() text = str(source_text or "") text_prefix = text[:4] digest = hashlib.sha256(text.encode("utf-8")).hexdigest() return f"trans:{normalized_model}:{normalized_target_lang}:{text_prefix}{digest}" def get( self, *, model: str, target_lang: str, source_text: str ) -> Optional[str]: if self.redis_client is None: return None key = self.build_key(model=model, target_lang=target_lang, source_text=source_text) try: value = self.redis_client.get(key) logger.info( "Translation cache %s | text_len=%s key=%s", "hit" if value is not None else "miss", len(str(source_text or "")), key, ) if value and self.sliding_expiration: self.redis_client.expire(key, self.ttl_seconds) return value except Exception as exc: logger.warning("Redis get translation cache failed: %s", exc) return None def set(self, *, model: str, target_lang: str, source_text: str, translated_text: str) -> None: if self.redis_client is None: return key = self.build_key(model=model, target_lang=target_lang, source_text=source_text) try: self.redis_client.setex(key, self.ttl_seconds, translated_text) logger.info( "Translation cache write | text_len=%s result_len=%s ttl_seconds=%s key=%s", len(str(source_text or "")), len(str(translated_text or "")), self.ttl_seconds, key, ) except Exception as exc: logger.warning("Redis set translation cache failed: %s", exc) @staticmethod def _init_redis_client() -> Optional[redis.Redis]: if redis is None: logger.warning("redis package is not installed; translation cache disabled") return None redis_config = get_app_config().infrastructure.redis try: client = redis.Redis( host=redis_config.host, port=redis_config.port, db=redis_config.snapshot_db, password=redis_config.password, decode_responses=True, socket_timeout=redis_config.socket_timeout, socket_connect_timeout=redis_config.socket_connect_timeout, retry_on_timeout=redis_config.retry_on_timeout, health_check_interval=10, ) client.ping() return client except Exception as exc: logger.warning("Failed to initialize translation redis cache: %s", exc) return None