190 lines
7.0 KiB
Python
190 lines
7.0 KiB
Python
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) |