# -*- coding: utf-8 -*- """ Django Translatable Fields - DRF Serializer Support This module provides Django REST Framework serializer mixins and fields for handling translatable fields in API responses and requests. Key Features: - Automatic language detection from request parameters - Language-specific field serialization and deserialization - Support for both single-language and multi-language API responses - Easy integration with existing DRF serializers Usage: from django_translatable_fields.serializers import TranslatableSerializerMixin class ProductSerializer(TranslatableSerializerMixin, serializers.ModelSerializer): class Meta: model = Product fields = ['id', 'name', 'description', 'price'] # API Usage: # GET /api/products/?language=de -> Returns German translations # GET /api/products/ -> Returns all translations as dicts # POST /api/products/ with {"name": "Test", "language": "de"} -> Saves to German Author: Holger Sielaff Version: 0.1.0 """ import json from rest_framework import serializers from django.utils.translation import get_language class TranslatableSerializerMixin: """ Mixin for DRF serializers that automatically handles translatable fields. This mixin provides automatic language detection and processing for fields that have the `translatable=True` attribute. It supports both reading and writing translatable content via API endpoints. Features: - Language parameter detection (?language=de) - Automatic field conversion for translatable fields - Support for both single-language and multi-language responses - Proper handling of JSON storage format Usage: class MySerializer(TranslatableSerializerMixin, serializers.ModelSerializer): class Meta: model = MyModel fields = ['name', 'description'] # name and description are translatable # API calls: # GET /api/items/?language=de -> returns German values # GET /api/items/ -> returns full translation dicts # POST /api/items/ with language param -> saves to specific language """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._setup_translatable_fields() def _setup_translatable_fields(self): """Identify and setup translatable fields in the serializer""" self._translatable_fields = [] if hasattr(self, 'Meta') and hasattr(self.Meta, 'model'): model = self.Meta.model for field_name in self.fields: if hasattr(model, field_name): model_field = model._meta.get_field(field_name) if hasattr(model_field, 'translatable') and model_field.translatable: self._translatable_fields.append(field_name) def _get_request_language(self): """Get the language parameter from the request context""" if hasattr(self, 'context') and 'request' in self.context: request = self.context['request'] # Check for language parameter in query params or data language = request.query_params.get('language') if not language and hasattr(request, 'data'): language = request.data.get('language') return language return None def to_representation(self, instance): """ Convert model instance to API representation. Logic: - With language parameter: returns single-language values - Without language parameter: * If DB value is dict -> return dict * If DB value is not dict -> return the value directly (assume current language) """ data = super().to_representation(instance) request_language = self._get_request_language() # Process translatable fields for field_name in self._translatable_fields: if field_name in data: field_value = data[field_name] # Handle JSON string from database if isinstance(field_value, str): try: parsed_value = json.loads(field_value) if isinstance(parsed_value, dict): field_value = parsed_value # If JSON but not dict, keep as string except (json.JSONDecodeError, TypeError): # If not JSON, keep as string pass # Return specific language or handle based on type if request_language: # Language specified: extract specific language value if isinstance(field_value, dict): # Return value for requested language, fallback to English, then first available if request_language in field_value: data[field_name] = field_value[request_language] elif 'en' in field_value: data[field_name] = field_value['en'] elif field_value: data[field_name] = next(iter(field_value.values())) else: data[field_name] = '' else: # Single value, return as-is data[field_name] = field_value else: # No language specified: # - If dict: return dict (full translations) # - If not dict: return value as-is (current language) data[field_name] = field_value return data def to_internal_value(self, data): """ Convert API input data to internal representation. Logic: - With language parameter: converts single values to {language: value} - Without language parameter: * If value is dict -> use dict as-is * If value is not dict -> convert to {'en': value} """ request_language = self._get_request_language() # Process translatable fields in input data for field_name in self._translatable_fields: if field_name in data: field_value = data[field_name] if request_language: # Convert single value to translation dict for specific language if not isinstance(field_value, dict): data[field_name] = {request_language: str(field_value)} # If it's already a dict, use it as-is else: # No language specified: # - If dict: use as-is (full translations) # - If not dict: assume English if not isinstance(field_value, dict): data[field_name] = {'en': str(field_value)} # If it's already a dict, leave it unchanged return super().to_internal_value(data) class TranslatableField(serializers.Field): """ Custom DRF field for handling translatable content. This field can be used when you need more control over how translatable fields are handled in your serializer, or when you want to use it independently of the TranslatableSerializerMixin. Usage: class ProductSerializer(serializers.ModelSerializer): name = TranslatableField() description = TranslatableField() class Meta: model = Product fields = ['id', 'name', 'description', 'price'] """ def __init__(self, **kwargs): self.language = kwargs.pop('language', None) super().__init__(**kwargs) def to_representation(self, value): """Convert database value to API representation""" if not value: return '' if self.language else {} # Handle JSON string from database if isinstance(value, str): try: translations = json.loads(value) except (json.JSONDecodeError, TypeError): translations = {'en': value} elif isinstance(value, dict): translations = value else: translations = {'en': str(value)} # Return specific language or full dict if self.language: if self.language in translations: return translations[self.language] elif 'en' in translations: return translations['en'] elif translations: return next(iter(translations.values())) else: return '' else: return translations def to_internal_value(self, data): """Convert API input to internal representation""" if self.language: # Convert single value to translation dict if isinstance(data, dict): return data else: return {self.language: str(data)} else: # Expect full translation dict if isinstance(data, dict): return data else: return {'en': str(data)} class TranslatableCharField(TranslatableField, serializers.CharField): """Translatable CharField for DRF serializers""" pass class TranslatableTextField(TranslatableField, serializers.CharField): """Translatable TextField for DRF serializers""" pass class TranslatableEmailField(TranslatableField, serializers.EmailField): """Translatable EmailField for DRF serializers""" pass class TranslatableURLField(TranslatableField, serializers.URLField): """Translatable URLField for DRF serializers""" pass class TranslatableSlugField(TranslatableField, serializers.SlugField): """Translatable SlugField for DRF serializers""" pass