or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

access-control.mdconfiguration.mddjango-signals.mdfile-operations.mdindex.mdstorage-backends.mdtemplate-integration.mdtranslation-services.mdweb-interface.md

django-signals.mddocs/

0

# Django Signals

1

2

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.

3

4

## Capabilities

5

6

### Translation Entry Change Signal

7

8

Signal fired when individual translation entries are modified, providing detailed information about the change.

9

10

```python { .api }

11

entry_changed = django.dispatch.Signal()

12

"""

13

Signal fired when translation entry is modified.

14

15

Sent whenever a translation entry (msgstr) is changed in the web interface,

16

allowing custom code to respond to translation modifications.

17

18

Signal Arguments:

19

- sender: The view class that triggered the change

20

- user: Django User instance who made the change

21

- old_msgstr: Previous translation string value

22

- old_fuzzy: Previous fuzzy flag state (boolean)

23

- pofile: POFile object representing the .po file

24

- language_code: Language code of the modified translation

25

- request: Django HttpRequest instance

26

27

Usage:

28

@receiver(entry_changed)

29

def handle_entry_changed(sender, **kwargs):

30

user = kwargs['user']

31

old_msgstr = kwargs['old_msgstr']

32

language_code = kwargs['language_code']

33

# Custom logic here

34

"""

35

36

@receiver(entry_changed)

37

def log_translation_changes(sender, **kwargs):

38

"""

39

Example signal handler for logging translation changes.

40

41

Parameters from signal:

42

- sender: View class

43

- user: User who made the change

44

- old_msgstr: Previous translation

45

- old_fuzzy: Previous fuzzy state

46

- pofile: POFile object

47

- language_code: Language being translated

48

- request: HTTP request

49

"""

50

```

51

52

### Post-Save Signal

53

54

Signal fired after .po files are saved to disk, enabling post-processing workflows.

55

56

```python { .api }

57

post_save = django.dispatch.Signal()

58

"""

59

Signal fired after .po file is saved to disk.

60

61

Sent after translation changes are committed to the .po file, allowing

62

custom code to perform post-processing, notifications, or integrations.

63

64

Signal Arguments:

65

- sender: The view class that saved the file

66

- language_code: Language code of the saved file

67

- request: Django HttpRequest instance

68

69

Usage:

70

@receiver(post_save)

71

def handle_po_file_saved(sender, **kwargs):

72

language_code = kwargs['language_code']

73

request = kwargs['request']

74

# Custom post-save logic here

75

"""

76

77

@receiver(post_save)

78

def compile_mo_files(sender, **kwargs):

79

"""

80

Example signal handler for compiling .mo files after .po save.

81

82

Parameters from signal:

83

- sender: View class

84

- language_code: Language that was saved

85

- request: HTTP request

86

"""

87

```

88

89

## Usage Examples

90

91

### Basic Signal Handlers

92

93

```python

94

# myapp/signals.py

95

from django.dispatch import receiver

96

from rosetta.signals import entry_changed, post_save

97

import logging

98

99

logger = logging.getLogger('translation_activity')

100

101

@receiver(entry_changed)

102

def log_translation_activity(sender, **kwargs):

103

"""Log all translation changes for audit purposes."""

104

105

user = kwargs['user']

106

old_msgstr = kwargs['old_msgstr']

107

language_code = kwargs['language_code']

108

pofile = kwargs['pofile']

109

110

# Find the changed entry (simplified example)

111

for entry in pofile:

112

if entry.msgstr != old_msgstr and entry.msgstr: # New translation

113

logger.info(

114

f"Translation changed by {user.username}: "

115

f"'{entry.msgid}' -> '{entry.msgstr}' ({language_code})"

116

)

117

break

118

119

@receiver(post_save)

120

def notify_translation_team(sender, **kwargs):

121

"""Send notifications when translations are saved."""

122

123

language_code = kwargs['language_code']

124

request = kwargs['request']

125

126

# Send email notification (example)

127

from django.core.mail import send_mail

128

129

send_mail(

130

subject=f'Translations updated for {language_code}',

131

message=f'User {request.user.username} has updated {language_code} translations.',

132

from_email='noreply@example.com',

133

recipient_list=['translation-team@example.com'],

134

fail_silently=True

135

)

136

```

137

138

### Integration with Version Control

139

140

```python

141

# myapp/signals.py

142

from django.dispatch import receiver

143

from rosetta.signals import post_save

144

import subprocess

145

import os

146

147

@receiver(post_save)

148

def auto_commit_translations(sender, **kwargs):

149

"""Automatically commit translation changes to git."""

150

151

language_code = kwargs['language_code']

152

request = kwargs['request']

153

154

try:

155

# Navigate to project root

156

project_root = '/path/to/your/project'

157

os.chdir(project_root)

158

159

# Add all .po files

160

subprocess.run(['git', 'add', '*.po'], check=True)

161

162

# Commit with descriptive message

163

commit_message = f"Update {language_code} translations by {request.user.username}"

164

subprocess.run(['git', 'commit', '-m', commit_message], check=True)

165

166

logger.info(f"Auto-committed {language_code} translations")

167

168

except subprocess.CalledProcessError as e:

169

logger.error(f"Failed to auto-commit translations: {e}")

170

171

@receiver(entry_changed)

172

def track_translation_quality(sender, **kwargs):

173

"""Track translation quality metrics."""

174

175

user = kwargs['user']

176

old_msgstr = kwargs['old_msgstr']

177

old_fuzzy = kwargs['old_fuzzy']

178

pofile = kwargs['pofile']

179

180

# Find the changed entry

181

for entry in pofile:

182

if hasattr(entry, 'msgstr') and entry.msgstr != old_msgstr:

183

# Track quality changes

184

if old_fuzzy and not entry.fuzzy:

185

# Fuzzy entry was clarified

186

record_quality_improvement(user, entry, 'fuzzy_resolved')

187

elif not old_msgstr and entry.msgstr:

188

# New translation added

189

record_quality_improvement(user, entry, 'translation_added')

190

elif old_msgstr and not entry.msgstr:

191

# Translation removed

192

record_quality_decline(user, entry, 'translation_removed')

193

break

194

195

def record_quality_improvement(user, entry, action_type):

196

"""Record translation quality improvements."""

197

# Implementation depends on your quality tracking system

198

pass

199

200

def record_quality_decline(user, entry, action_type):

201

"""Record translation quality declines."""

202

# Implementation depends on your quality tracking system

203

pass

204

```

205

206

### Custom Workflow Integration

207

208

```python

209

# myapp/signals.py

210

from django.dispatch import receiver

211

from rosetta.signals import entry_changed, post_save

212

from myapp.models import TranslationWorkflow, TranslationReview

213

214

@receiver(entry_changed)

215

def trigger_review_workflow(sender, **kwargs):

216

"""Trigger review workflow for translation changes."""

217

218

user = kwargs['user']

219

language_code = kwargs['language_code']

220

pofile = kwargs['pofile']

221

222

# Create review request for significant changes

223

workflow, created = TranslationWorkflow.objects.get_or_create(

224

language_code=language_code,

225

status='in_progress'

226

)

227

228

# Add review item

229

TranslationReview.objects.create(

230

workflow=workflow,

231

translator=user,

232

po_file_path=pofile.fpath,

233

status='pending_review'

234

)

235

236

@receiver(post_save)

237

def update_translation_cache(sender, **kwargs):

238

"""Update cached translation statistics."""

239

240

language_code = kwargs['language_code']

241

242

# Clear relevant caches

243

from django.core.cache import cache

244

cache_keys = [

245

f'translation_stats_{language_code}',

246

f'translation_progress_{language_code}',

247

'translation_overview'

248

]

249

250

for key in cache_keys:

251

cache.delete(key)

252

253

# Regenerate statistics asynchronously

254

from myapp.tasks import update_translation_statistics

255

update_translation_statistics.delay(language_code)

256

257

@receiver(post_save)

258

def backup_translations(sender, **kwargs):

259

"""Create backup of translation files."""

260

261

language_code = kwargs['language_code']

262

request = kwargs['request']

263

264

from myapp.utils import create_translation_backup

265

266

try:

267

backup_path = create_translation_backup(language_code)

268

logger.info(f"Created translation backup: {backup_path}")

269

270

# Optionally store backup metadata

271

from myapp.models import TranslationBackup

272

TranslationBackup.objects.create(

273

language_code=language_code,

274

backup_path=backup_path,

275

created_by=request.user,

276

reason='auto_save'

277

)

278

279

except Exception as e:

280

logger.error(f"Failed to create translation backup: {e}")

281

```

282

283

### Metrics and Analytics

284

285

```python

286

# myapp/signals.py

287

from django.dispatch import receiver

288

from rosetta.signals import entry_changed, post_save

289

from django.utils import timezone

290

from myapp.models import TranslationMetrics

291

292

@receiver(entry_changed)

293

def track_translation_metrics(sender, **kwargs):

294

"""Track detailed translation activity metrics."""

295

296

user = kwargs['user']

297

old_msgstr = kwargs['old_msgstr']

298

language_code = kwargs['language_code']

299

300

# Determine action type

301

if not old_msgstr:

302

action_type = 'translation_added'

303

elif not kwargs.get('new_msgstr'): # Would need to get this from entry

304

action_type = 'translation_removed'

305

else:

306

action_type = 'translation_modified'

307

308

# Record metrics

309

TranslationMetrics.objects.create(

310

user=user,

311

language_code=language_code,

312

action_type=action_type,

313

timestamp=timezone.now()

314

)

315

316

@receiver(post_save)

317

def update_project_statistics(sender, **kwargs):

318

"""Update project-wide translation statistics."""

319

320

language_code = kwargs['language_code']

321

322

# Calculate updated statistics

323

from rosetta.poutil import find_pos

324

325

files = find_pos(language_code)

326

327

total_translated = 0

328

total_untranslated = 0

329

total_fuzzy = 0

330

331

for file_info in files:

332

stats = file_info['stats']

333

total_translated += stats.get('translated', 0)

334

total_untranslated += stats.get('untranslated', 0)

335

total_fuzzy += stats.get('fuzzy', 0)

336

337

# Store in cache for quick access

338

from django.core.cache import cache

339

cache.set(f'project_translation_stats_{language_code}', {

340

'translated': total_translated,

341

'untranslated': total_untranslated,

342

'fuzzy': total_fuzzy,

343

'completion_percentage': (total_translated / (total_translated + total_untranslated + total_fuzzy)) * 100 if (total_translated + total_untranslated + total_fuzzy) > 0 else 0,

344

'last_updated': timezone.now().isoformat()

345

}, timeout=3600) # Cache for 1 hour

346

```

347

348

### Signal Registration

349

350

```python

351

# myapp/apps.py

352

from django.apps import AppConfig

353

354

class MyAppConfig(AppConfig):

355

default_auto_field = 'django.db.models.BigAutoField'

356

name = 'myapp'

357

358

def ready(self):

359

# Import signal handlers to register them

360

import myapp.signals

361

362

# myapp/__init__.py

363

default_app_config = 'myapp.apps.MyAppConfig'

364

```

365

366

### Testing Signal Handlers

367

368

```python

369

# myapp/tests.py

370

from django.test import TestCase, RequestFactory

371

from django.contrib.auth.models import User

372

from rosetta.signals import entry_changed, post_save

373

from unittest.mock import Mock, patch

374

375

class SignalHandlerTests(TestCase):

376

def setUp(self):

377

self.factory = RequestFactory()

378

self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')

379

380

def test_entry_changed_signal(self):

381

"""Test entry_changed signal is properly handled."""

382

383

request = self.factory.post('/')

384

request.user = self.user

385

386

# Mock POFile object

387

pofile = Mock()

388

pofile.fpath = '/path/to/test.po'

389

390

# Send signal

391

with patch('myapp.signals.logger') as mock_logger:

392

entry_changed.send(

393

sender=Mock,

394

user=self.user,

395

old_msgstr='old translation',

396

old_fuzzy=True,

397

pofile=pofile,

398

language_code='fr',

399

request=request

400

)

401

402

# Verify handler was called

403

mock_logger.info.assert_called()

404

405

def test_post_save_signal(self):

406

"""Test post_save signal is properly handled."""

407

408

request = self.factory.post('/')

409

request.user = self.user

410

411

# Send signal

412

with patch('myapp.signals.send_mail') as mock_send_mail:

413

post_save.send(

414

sender=Mock,

415

language_code='es',

416

request=request

417

)

418

419

# Verify notification was sent

420

mock_send_mail.assert_called_once()

421

```

422

423

### Signal Debugging

424

425

```python

426

# myapp/signals.py

427

import logging

428

from django.dispatch import receiver

429

from rosetta.signals import entry_changed, post_save

430

431

# Configure detailed logging for signal debugging

432

signal_logger = logging.getLogger('rosetta.signals')

433

434

@receiver(entry_changed)

435

def debug_entry_changed(sender, **kwargs):

436

"""Debug handler for entry_changed signal."""

437

438

signal_logger.debug(f"entry_changed signal received:")

439

signal_logger.debug(f" sender: {sender}")

440

signal_logger.debug(f" user: {kwargs.get('user')}")

441

signal_logger.debug(f" old_msgstr: {kwargs.get('old_msgstr')}")

442

signal_logger.debug(f" old_fuzzy: {kwargs.get('old_fuzzy')}")

443

signal_logger.debug(f" language_code: {kwargs.get('language_code')}")

444

signal_logger.debug(f" pofile: {kwargs.get('pofile')}")

445

446

@receiver(post_save)

447

def debug_post_save(sender, **kwargs):

448

"""Debug handler for post_save signal."""

449

450

signal_logger.debug(f"post_save signal received:")

451

signal_logger.debug(f" sender: {sender}")

452

signal_logger.debug(f" language_code: {kwargs.get('language_code')}")

453

signal_logger.debug(f" request.user: {kwargs.get('request', {}).user if kwargs.get('request') else 'None'}")

454

455

# In settings.py for debugging

456

LOGGING = {

457

'version': 1,

458

'disable_existing_loggers': False,

459

'handlers': {

460

'console': {

461

'class': 'logging.StreamHandler',

462

},

463

},

464

'loggers': {

465

'rosetta.signals': {

466

'handlers': ['console'],

467

'level': 'DEBUG',

468

'propagate': True,

469

},

470

},

471

}

472

```