# -*- 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 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('
') # 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''' ''') # 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'') 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'') output.append('
') 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)