Initial
This commit is contained in:
190
django_translatable_fields/descriptors.py
Normal file
190
django_translatable_fields/descriptors.py
Normal file
@@ -0,0 +1,190 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user