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

276 lines
10 KiB
Python
Raw Normal View History

2025-08-02 20:08:33 +02:00
# -*- coding: utf-8 -*-
"""
Django Translatable Fields - Custom Widgets
This module provides custom Django form widgets for editing translatable fields
in the admin interface and forms. The widgets create a modal overlay system
that allows users to edit translations for multiple languages.
Key Features:
- Modal overlay for editing multiple language translations
- Automatic widget type detection (TextInput vs Textarea)
- JavaScript integration for dynamic UI
- CSS styling for professional appearance
- Raw database value retrieval for proper translation display
Components:
- TranslatableWidget: Base widget with modal functionality
- TranslatableTextWidget: For CharField-like inputs
- TranslatableTextareaWidget: For TextField-like inputs
The widgets automatically detect existing translations and populate the modal
with current values, allowing seamless editing of multilingual content.
Author: Holger Sielaff <holger@backender.de>
Version: 0.1.0
"""
import json
from django import forms
from django.conf import settings
from django.utils.translation import get_language, gettext_lazy as _
from django.utils.safestring import mark_safe
class TranslatableWidget(forms.Widget):
"""
Base widget for translatable fields with modal overlay functionality.
This widget creates a user interface that displays a primary input field
(usually for the default/current language) along with a translate button
that opens a modal overlay for editing translations in other languages.
Features:
- Primary field for current language (visible)
- Hidden fields for other language translations
- Modal overlay with language-specific inputs
- Automatic widget type detection (TextInput vs Textarea)
- JavaScript integration for modal functionality
- CSS styling for professional appearance
The widget automatically:
- Detects available languages from Django settings
- Retrieves existing translations from database
- Handles form submission for all languages
- Provides fallback values when translations are missing
Args:
original_widget: The Django widget to use for input fields
languages: List of (code, name) language tuples
attrs: Standard Django widget attributes
Example:
# Used automatically by translatable form fields
widget = TranslatableWidget(original_widget=forms.TextInput())
"""
def __init__(self, original_widget=None, languages=None, attrs=None):
# Determine widget class from original widget or default to TextInput
if original_widget:
self.widget_class = original_widget.__class__
self.is_textarea = isinstance(original_widget, forms.Textarea)
else:
self.widget_class = forms.TextInput
self.is_textarea = False
self.languages = languages or getattr(settings, 'LANGUAGES', [('en', 'English')])
# Ensure English is first
self.languages = [lang for lang in self.languages if lang[0] == 'en'] + [lang for lang in self.languages if lang[0] != 'en']
if not any(lang[0] == 'en' for lang in self.languages):
self.languages = [('en', 'English')] + list(self.languages)
self.primary_widget = self.widget_class(attrs=attrs)
self._bound_field = None # Store reference to bound field
super().__init__(attrs)
def format_value(self, value):
"""
Format value for display in the primary input field.
This method extracts the appropriate language value from the translation
dictionary for display in the main input field (usually English).
Args:
value: Translation dictionary, JSON string, or plain value
Returns:
str: Formatted value for primary field display
"""
if not value:
return ''
# Parse JSON strings to dictionaries
if isinstance(value, str):
try:
value = json.loads(value)
except (json.JSONDecodeError, TypeError):
# If not JSON, return as plain string
return value
# Extract English value for primary display
if isinstance(value, dict):
# Prefer English, fallback to first available value
return value.get('en', list(value.values())[0] if value else '')
# For non-dict values, convert to string
return str(value)
def value_from_datadict(self, data, files, name):
# Get primary field value (English)
primary_value = data.get(name, '')
# Get other language values
values = {}
# Always set English from primary field if present
if primary_value:
values['en'] = primary_value
# Get other language values from hidden fields
for lang_code, lang_name in self.languages:
if lang_code != 'en':
field_name = f'{name}_{lang_code}'
lang_value = data.get(field_name, '')
if lang_value: # Only add if not empty
values[lang_code] = lang_value
return values if values else None
def render(self, name, value, attrs=None, renderer=None):
if attrs is None:
attrs = {}
# Debug: Print what we're getting as value
print(f"Widget render - name: {name}, value: {repr(value)}, type: {type(value)}")
# Try to get the full translations from the current context
translations = {}
# Try to parse value first
if isinstance(value, dict):
translations = value
elif isinstance(value, str):
try:
parsed = json.loads(value)
if isinstance(parsed, dict):
translations = parsed
else:
translations = {'en': value}
except (json.JSONDecodeError, TypeError):
translations = {'en': value}
elif value:
translations = {'en': str(value)}
print(f"Widget render - parsed translations from value: {translations}")
# If translations are empty or only have English, try to get from the bound instance
if len(translations) <= 1 and hasattr(self, '_instance_data'):
instance_data = self._instance_data
field_name = instance_data.get('field_name')
instance = instance_data.get('instance')
if instance and field_name and hasattr(instance, 'pk') and instance.pk:
try:
# Get the raw database value for this specific instance
from django.db import connection
cursor = connection.cursor()
table_name = instance._meta.db_table
cursor.execute(f'SELECT {field_name} FROM {table_name} WHERE id = %s', [instance.pk])
row = cursor.fetchone()
if row and row[0]:
try:
db_translations = json.loads(row[0])
if isinstance(db_translations, dict):
print(f"Widget render - got translations for instance {instance.pk}: {db_translations}")
translations = db_translations
except (json.JSONDecodeError, TypeError):
pass
except Exception as e:
print(f"Widget render - instance DB lookup error: {e}")
print(f"Widget render - final translations: {translations}")
# Get English value for primary field
primary_value = translations.get('en', '')
# Create unique ID for this field
field_id = attrs.get('id', name)
output = []
# Primary field (English) with translate button
output.append('<div class="translatable-field-container">')
# Primary input field
primary_attrs = attrs.copy()
primary_attrs['id'] = field_id
primary_attrs['class'] = primary_attrs.get('class', '') + ' translatable-primary-field'
primary_html = self.primary_widget.render(name, primary_value, primary_attrs)
output.append(primary_html)
# Translate button
output.append(f'''
<button type="button" class="translatable-button" onclick="openTranslateModal('{field_id}')" title="Translate">
🌐
</button>
<!-- Hidden fields for other languages -->
''')
# Hidden fields for other languages
for lang_code, lang_name in self.languages:
if lang_code != 'en':
lang_value = translations.get(lang_code, '')
# Debug: Add comment to HTML to see what values we're rendering
output.append(f'<!-- Hidden field for {lang_code}: value="{lang_value}" -->')
lang_attrs = {
'type': 'hidden',
'id': f'{field_id}_{lang_code}',
'name': f'{name}_{lang_code}',
'value': lang_value,
'data-lang': lang_code,
'data-lang-name': lang_name
}
output.append(f'<input {" ".join(f"{k}=\"{v}\"" for k, v in lang_attrs.items())}>')
output.append('</div>')
return mark_safe(''.join(output))
@property
def media(self):
return forms.Media(
css={'all': ['django_translatable_fields/translatable.css']},
js=['django_translatable_fields/translatable.js']
)
class TranslatableTextWidget(TranslatableWidget):
"""
Widget for translatable text inputs.
"""
def __init__(self, languages=None, attrs=None):
# Create a TextInput widget as the original widget
original_widget = forms.TextInput(attrs=attrs)
super().__init__(original_widget=original_widget, languages=languages, attrs=attrs)
class TranslatableTextareaWidget(TranslatableWidget):
"""
Widget for translatable textarea inputs.
"""
def __init__(self, languages=None, attrs=None):
# Create a Textarea widget as the original widget
original_widget = forms.Textarea(attrs=attrs)
super().__init__(original_widget=original_widget, languages=languages, attrs=attrs)