import json import os from enum import Enum from typing import Any, Dict, List, Optional class SupportedLanguage(Enum): EN = "en" ES = "es" FR = "fr" DE = "de" IT = "it" PT = "pt" RU = "ru" JA = "ja" KO = "ko" HI = "hi" ID = "id" @classmethod def from_string(cls, value: str) -> Optional["SupportedLanguage"]: for language in cls: if language.value == value.lower(): return language return None class KeyBasedTranslator: """ A simple key-based translator that loads translations from JSON files. """ _translations: Dict[SupportedLanguage, Dict[str, Any]] = {} def __init__(self, custom_translations: Optional[Dict[SupportedLanguage, Dict[str, Any]]] = None): self.custom_translations = custom_translations def get_translations(self, language: SupportedLanguage) -> Dict[str, Any]: if self.custom_translations is None or language not in self.custom_translations: return self.load_translations(language) return self.custom_translations[language] @classmethod def load_translations(cls, language: SupportedLanguage) -> Dict[str, Any]: """ Load translations for a given language from JSON file. """ if language in cls._translations: return cls._translations[language] # Try to load from JSON file json_path = os.path.join("locales", language.value, "translations.json") if os.path.exists(json_path): try: with open(json_path, "r", encoding="utf-8") as f: translations = json.load(f) cls._translations[language] = translations return translations except (json.JSONDecodeError, IOError): pass # Fallback to empty dict cls._translations[language] = {} return cls._translations[language] def _translate(self, key: str, language: Optional[SupportedLanguage] = None) -> Optional[str]: if language is None: language = SupportedLanguage.EN translations = self.get_translations(language) # Support nested keys with dot notation (e.g., "user.profile.name") value = translations for part in key.split("."): if isinstance(value, dict) and part in value: value = value[part] else: return None return str(value) if value is not None else None def translate( self, key: str, language: Optional[SupportedLanguage] = None, default: Optional[str] = None ) -> str: translation = self._translate(key, language) if translation is None and default is not None: return default if translation is None and language != SupportedLanguage.EN: translation = self._translate(key, SupportedLanguage.EN) return translation if translation is not None else key _default_translator = KeyBasedTranslator() def _t( key: Optional[str], language: Optional[SupportedLanguage] = None, default: Optional[str] = None ) -> str: """ Convenience function for translating keys. """ if key is None: return "" return _default_translator.translate(key, language, default) def get_language_abbr_from_accept_language(request) -> List[str]: """Extract language abbreviations from Accept-Language header.""" accept_language = getattr(request, "headers", {}).get("accept-language", "") if hasattr(request, "headers"): accept_language = request.headers.get("accept-language", "") else: # Fallback for different request types accept_language = getattr(request, "accept_language", "") language_abbrs = [] if accept_language: # Split by comma to separate different language tags language_tags = accept_language.split(",") for tag in language_tags: # Split by semicolon to remove quality values (e.g., q=0.9) language_code = tag.split(";")[0].strip() # Extract just the language part (e.g., "en" from "en-US") language_abbr = language_code.split("-")[0].lower() language_abbrs.append(language_abbr) return language_abbrs def resolve_preferred_language_from_request( request, default: SupportedLanguage = SupportedLanguage.EN ) -> SupportedLanguage: """Resolve the user's preferred language from Accept-Language header.""" try: abbr_list = get_language_abbr_from_accept_language(request) except Exception: abbr_list = [] for abbr in abbr_list: lang = SupportedLanguage.from_string(abbr) if lang is not None: return lang return default