import json from django.utils.translation import get_language class TranslatableFieldDescriptor: """ Descriptor that provides language-aware access to translatable field values. Usage: class MyModel(models.Model): name = TranslatableCharField() # Access current language value obj.name # Returns value in current language # Access specific language obj.name_en # Returns English value obj.name_de # Returns German value # Set values obj.name = "Hello" # Sets for current language obj.name_en = "Hello" # Sets for English obj.name_de = "Hallo" # Sets for German """ def __init__(self, field_name): self.field_name = field_name self.storage_name = f'_{field_name}_translations' def __get__(self, instance, owner=None): if instance is None: return self # Get the stored translations raw_value = getattr(instance, self.field_name) translations = self._parse_translations(raw_value) # Return value for current language current_lang = get_language() if current_lang and current_lang in translations: return translations[current_lang] # Fallback to default language or first available if 'en' in translations: return translations['en'] elif translations: return next(iter(translations.values())) return '' def __set__(self, instance, value): if value is None: setattr(instance, self.field_name, None) return # Get existing translations raw_value = getattr(instance, self.field_name, None) translations = self._parse_translations(raw_value) if isinstance(value, dict): # Setting multiple translations at once translations.update(value) else: # Setting value for current language current_lang = get_language() or 'en' translations[current_lang] = str(value) # Store back as JSON setattr(instance, self.field_name, json.dumps(translations)) def _parse_translations(self, value): """Parse stored translations from various formats.""" if not value: return {} if isinstance(value, dict): return value if isinstance(value, str): try: parsed = json.loads(value) if isinstance(parsed, dict): return parsed except (json.JSONDecodeError, TypeError): pass # If it's a plain string, treat as current language value current_lang = get_language() or 'en' return {current_lang: value} return {} class TranslatableModelMixin: """ Mixin that adds language-specific property access to models. This allows accessing specific language versions of translatable fields: obj.field_name_en, obj.field_name_de, etc. """ def __getattr__(self, name): # Check if this is a language-specific field access if '_' in name: field_name, lang_code = name.rsplit('_', 1) # Check if the base field exists and is translatable if hasattr(self._meta.model, field_name): field = self._meta.get_field(field_name) if hasattr(field, 'translatable') and field.translatable: raw_value = getattr(self, field_name) translations = self._parse_field_translations(raw_value) return translations.get(lang_code, '') raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") def __setattr__(self, name, value): # Check if this is a language-specific field access if '_' in name and not name.startswith('_'): field_name, lang_code = name.rsplit('_', 1) # Check if the base field exists and is translatable if hasattr(self._meta.model, field_name): try: field = self._meta.get_field(field_name) if hasattr(field, 'translatable') and field.translatable: # Get existing translations raw_value = getattr(self, field_name, None) translations = self._parse_field_translations(raw_value) # Update the specific language translations[lang_code] = str(value) if value is not None else '' # Store back as JSON super().__setattr__(field_name, json.dumps(translations)) return except: # If there's any error, fall back to normal attribute setting pass super().__setattr__(name, value) def _parse_field_translations(self, value): """Parse stored translations from various formats.""" if not value: return {} if isinstance(value, dict): return value if isinstance(value, str): try: parsed = json.loads(value) if isinstance(parsed, dict): return parsed except (json.JSONDecodeError, TypeError): pass # If it's a plain string, treat as current language value current_lang = get_language() or 'en' return {current_lang: value} return {} def get_translation(self, field_name, language_code): """Get translation for a specific field and language.""" if not hasattr(self, field_name): raise AttributeError(f"Field '{field_name}' does not exist") raw_value = getattr(self, field_name) translations = self._parse_field_translations(raw_value) return translations.get(language_code, '') def set_translation(self, field_name, language_code, value): """Set translation for a specific field and language.""" if not hasattr(self, field_name): raise AttributeError(f"Field '{field_name}' does not exist") raw_value = getattr(self, field_name, None) translations = self._parse_field_translations(raw_value) translations[language_code] = str(value) if value is not None else '' setattr(self, field_name, json.dumps(translations)) def get_all_translations(self, field_name): """Get all translations for a specific field.""" if not hasattr(self, field_name): raise AttributeError(f"Field '{field_name}' does not exist") raw_value = getattr(self, field_name) return self._parse_field_translations(raw_value)