Files
Django-Translatable-Fields/django_translatable_fields/serializers.py

264 lines
10 KiB
Python
Raw Normal View History

2025-08-02 20:08:33 +02:00
# -*- 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 <holger@backender.de>
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