A Django application that facilitates the translation process of your Django projects
—
Event system for extending Rosetta functionality and integrating with custom workflows through Django's signal framework. These signals provide hooks for monitoring translation activities, implementing custom business logic, and integrating with external systems.
Signal fired when individual translation entries are modified, providing detailed information about the change.
entry_changed = django.dispatch.Signal()
"""
Signal fired when translation entry is modified.
Sent whenever a translation entry (msgstr) is changed in the web interface,
allowing custom code to respond to translation modifications.
Signal Arguments:
- sender: The view class that triggered the change
- user: Django User instance who made the change
- old_msgstr: Previous translation string value
- old_fuzzy: Previous fuzzy flag state (boolean)
- pofile: POFile object representing the .po file
- language_code: Language code of the modified translation
- request: Django HttpRequest instance
Usage:
@receiver(entry_changed)
def handle_entry_changed(sender, **kwargs):
user = kwargs['user']
old_msgstr = kwargs['old_msgstr']
language_code = kwargs['language_code']
# Custom logic here
"""
@receiver(entry_changed)
def log_translation_changes(sender, **kwargs):
"""
Example signal handler for logging translation changes.
Parameters from signal:
- sender: View class
- user: User who made the change
- old_msgstr: Previous translation
- old_fuzzy: Previous fuzzy state
- pofile: POFile object
- language_code: Language being translated
- request: HTTP request
"""Signal fired after .po files are saved to disk, enabling post-processing workflows.
post_save = django.dispatch.Signal()
"""
Signal fired after .po file is saved to disk.
Sent after translation changes are committed to the .po file, allowing
custom code to perform post-processing, notifications, or integrations.
Signal Arguments:
- sender: The view class that saved the file
- language_code: Language code of the saved file
- request: Django HttpRequest instance
Usage:
@receiver(post_save)
def handle_po_file_saved(sender, **kwargs):
language_code = kwargs['language_code']
request = kwargs['request']
# Custom post-save logic here
"""
@receiver(post_save)
def compile_mo_files(sender, **kwargs):
"""
Example signal handler for compiling .mo files after .po save.
Parameters from signal:
- sender: View class
- language_code: Language that was saved
- request: HTTP request
"""# myapp/signals.py
from django.dispatch import receiver
from rosetta.signals import entry_changed, post_save
import logging
logger = logging.getLogger('translation_activity')
@receiver(entry_changed)
def log_translation_activity(sender, **kwargs):
"""Log all translation changes for audit purposes."""
user = kwargs['user']
old_msgstr = kwargs['old_msgstr']
language_code = kwargs['language_code']
pofile = kwargs['pofile']
# Find the changed entry (simplified example)
for entry in pofile:
if entry.msgstr != old_msgstr and entry.msgstr: # New translation
logger.info(
f"Translation changed by {user.username}: "
f"'{entry.msgid}' -> '{entry.msgstr}' ({language_code})"
)
break
@receiver(post_save)
def notify_translation_team(sender, **kwargs):
"""Send notifications when translations are saved."""
language_code = kwargs['language_code']
request = kwargs['request']
# Send email notification (example)
from django.core.mail import send_mail
send_mail(
subject=f'Translations updated for {language_code}',
message=f'User {request.user.username} has updated {language_code} translations.',
from_email='noreply@example.com',
recipient_list=['translation-team@example.com'],
fail_silently=True
)# myapp/signals.py
from django.dispatch import receiver
from rosetta.signals import post_save
import subprocess
import os
@receiver(post_save)
def auto_commit_translations(sender, **kwargs):
"""Automatically commit translation changes to git."""
language_code = kwargs['language_code']
request = kwargs['request']
try:
# Navigate to project root
project_root = '/path/to/your/project'
os.chdir(project_root)
# Add all .po files
subprocess.run(['git', 'add', '*.po'], check=True)
# Commit with descriptive message
commit_message = f"Update {language_code} translations by {request.user.username}"
subprocess.run(['git', 'commit', '-m', commit_message], check=True)
logger.info(f"Auto-committed {language_code} translations")
except subprocess.CalledProcessError as e:
logger.error(f"Failed to auto-commit translations: {e}")
@receiver(entry_changed)
def track_translation_quality(sender, **kwargs):
"""Track translation quality metrics."""
user = kwargs['user']
old_msgstr = kwargs['old_msgstr']
old_fuzzy = kwargs['old_fuzzy']
pofile = kwargs['pofile']
# Find the changed entry
for entry in pofile:
if hasattr(entry, 'msgstr') and entry.msgstr != old_msgstr:
# Track quality changes
if old_fuzzy and not entry.fuzzy:
# Fuzzy entry was clarified
record_quality_improvement(user, entry, 'fuzzy_resolved')
elif not old_msgstr and entry.msgstr:
# New translation added
record_quality_improvement(user, entry, 'translation_added')
elif old_msgstr and not entry.msgstr:
# Translation removed
record_quality_decline(user, entry, 'translation_removed')
break
def record_quality_improvement(user, entry, action_type):
"""Record translation quality improvements."""
# Implementation depends on your quality tracking system
pass
def record_quality_decline(user, entry, action_type):
"""Record translation quality declines."""
# Implementation depends on your quality tracking system
pass# myapp/signals.py
from django.dispatch import receiver
from rosetta.signals import entry_changed, post_save
from myapp.models import TranslationWorkflow, TranslationReview
@receiver(entry_changed)
def trigger_review_workflow(sender, **kwargs):
"""Trigger review workflow for translation changes."""
user = kwargs['user']
language_code = kwargs['language_code']
pofile = kwargs['pofile']
# Create review request for significant changes
workflow, created = TranslationWorkflow.objects.get_or_create(
language_code=language_code,
status='in_progress'
)
# Add review item
TranslationReview.objects.create(
workflow=workflow,
translator=user,
po_file_path=pofile.fpath,
status='pending_review'
)
@receiver(post_save)
def update_translation_cache(sender, **kwargs):
"""Update cached translation statistics."""
language_code = kwargs['language_code']
# Clear relevant caches
from django.core.cache import cache
cache_keys = [
f'translation_stats_{language_code}',
f'translation_progress_{language_code}',
'translation_overview'
]
for key in cache_keys:
cache.delete(key)
# Regenerate statistics asynchronously
from myapp.tasks import update_translation_statistics
update_translation_statistics.delay(language_code)
@receiver(post_save)
def backup_translations(sender, **kwargs):
"""Create backup of translation files."""
language_code = kwargs['language_code']
request = kwargs['request']
from myapp.utils import create_translation_backup
try:
backup_path = create_translation_backup(language_code)
logger.info(f"Created translation backup: {backup_path}")
# Optionally store backup metadata
from myapp.models import TranslationBackup
TranslationBackup.objects.create(
language_code=language_code,
backup_path=backup_path,
created_by=request.user,
reason='auto_save'
)
except Exception as e:
logger.error(f"Failed to create translation backup: {e}")# myapp/signals.py
from django.dispatch import receiver
from rosetta.signals import entry_changed, post_save
from django.utils import timezone
from myapp.models import TranslationMetrics
@receiver(entry_changed)
def track_translation_metrics(sender, **kwargs):
"""Track detailed translation activity metrics."""
user = kwargs['user']
old_msgstr = kwargs['old_msgstr']
language_code = kwargs['language_code']
# Determine action type
if not old_msgstr:
action_type = 'translation_added'
elif not kwargs.get('new_msgstr'): # Would need to get this from entry
action_type = 'translation_removed'
else:
action_type = 'translation_modified'
# Record metrics
TranslationMetrics.objects.create(
user=user,
language_code=language_code,
action_type=action_type,
timestamp=timezone.now()
)
@receiver(post_save)
def update_project_statistics(sender, **kwargs):
"""Update project-wide translation statistics."""
language_code = kwargs['language_code']
# Calculate updated statistics
from rosetta.poutil import find_pos
files = find_pos(language_code)
total_translated = 0
total_untranslated = 0
total_fuzzy = 0
for file_info in files:
stats = file_info['stats']
total_translated += stats.get('translated', 0)
total_untranslated += stats.get('untranslated', 0)
total_fuzzy += stats.get('fuzzy', 0)
# Store in cache for quick access
from django.core.cache import cache
cache.set(f'project_translation_stats_{language_code}', {
'translated': total_translated,
'untranslated': total_untranslated,
'fuzzy': total_fuzzy,
'completion_percentage': (total_translated / (total_translated + total_untranslated + total_fuzzy)) * 100 if (total_translated + total_untranslated + total_fuzzy) > 0 else 0,
'last_updated': timezone.now().isoformat()
}, timeout=3600) # Cache for 1 hour# myapp/apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'myapp'
def ready(self):
# Import signal handlers to register them
import myapp.signals
# myapp/__init__.py
default_app_config = 'myapp.apps.MyAppConfig'# myapp/tests.py
from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User
from rosetta.signals import entry_changed, post_save
from unittest.mock import Mock, patch
class SignalHandlerTests(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
def test_entry_changed_signal(self):
"""Test entry_changed signal is properly handled."""
request = self.factory.post('/')
request.user = self.user
# Mock POFile object
pofile = Mock()
pofile.fpath = '/path/to/test.po'
# Send signal
with patch('myapp.signals.logger') as mock_logger:
entry_changed.send(
sender=Mock,
user=self.user,
old_msgstr='old translation',
old_fuzzy=True,
pofile=pofile,
language_code='fr',
request=request
)
# Verify handler was called
mock_logger.info.assert_called()
def test_post_save_signal(self):
"""Test post_save signal is properly handled."""
request = self.factory.post('/')
request.user = self.user
# Send signal
with patch('myapp.signals.send_mail') as mock_send_mail:
post_save.send(
sender=Mock,
language_code='es',
request=request
)
# Verify notification was sent
mock_send_mail.assert_called_once()# myapp/signals.py
import logging
from django.dispatch import receiver
from rosetta.signals import entry_changed, post_save
# Configure detailed logging for signal debugging
signal_logger = logging.getLogger('rosetta.signals')
@receiver(entry_changed)
def debug_entry_changed(sender, **kwargs):
"""Debug handler for entry_changed signal."""
signal_logger.debug(f"entry_changed signal received:")
signal_logger.debug(f" sender: {sender}")
signal_logger.debug(f" user: {kwargs.get('user')}")
signal_logger.debug(f" old_msgstr: {kwargs.get('old_msgstr')}")
signal_logger.debug(f" old_fuzzy: {kwargs.get('old_fuzzy')}")
signal_logger.debug(f" language_code: {kwargs.get('language_code')}")
signal_logger.debug(f" pofile: {kwargs.get('pofile')}")
@receiver(post_save)
def debug_post_save(sender, **kwargs):
"""Debug handler for post_save signal."""
signal_logger.debug(f"post_save signal received:")
signal_logger.debug(f" sender: {sender}")
signal_logger.debug(f" language_code: {kwargs.get('language_code')}")
signal_logger.debug(f" request.user: {kwargs.get('request', {}).user if kwargs.get('request') else 'None'}")
# In settings.py for debugging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'rosetta.signals': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': True,
},
},
}Install with Tessl CLI
npx tessl i tessl/pypi-django-rosetta