Files

190 lines
7.0 KiB
Python
Raw Permalink Normal View History

2025-08-02 20:08:33 +02:00
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)