diff --git a/README.md b/README.md index b191e5e..d1ae335 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,12 @@ Notes: - Form Builder option labels - Form Builder field label/help-text overrides - Intro Builder checklist item labels -- Most email template business text and several generated PDF text blocks are still not fully bilingual yet. +- Admin-configured email templates are now bilingual for: + - notification template subjects and bodies in DE/EN + - notification rule custom subjects and bodies in DE/EN + - welcome email subject and body in DE/EN + - request language capture on onboarding/offboarding to choose the correct email language +- Several generated PDF business text blocks are still not fully bilingual yet. - CI now validates that translation catalogs compile successfully on push and pull request. ## Current implemented scope diff --git a/backend/workflows/migrations/0030_notificationrule_custom_body_en_and_more.py b/backend/workflows/migrations/0030_notificationrule_custom_body_en_and_more.py new file mode 100644 index 0000000..5880010 --- /dev/null +++ b/backend/workflows/migrations/0030_notificationrule_custom_body_en_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 5.1.5 on 2026-03-24 11:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0029_formfieldconfig_help_text_override_en_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='notificationrule', + name='custom_body_en', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='notificationrule', + name='custom_subject_en', + field=models.CharField(blank=True, max_length=255), + ), + migrations.AddField( + model_name='notificationtemplate', + name='body_template_en', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='notificationtemplate', + name='subject_template_en', + field=models.CharField(blank=True, max_length=255), + ), + migrations.AddField( + model_name='offboardingrequest', + name='preferred_language', + field=models.CharField(blank=True, default='de', max_length=10), + ), + migrations.AddField( + model_name='onboardingrequest', + name='preferred_language', + field=models.CharField(blank=True, default='de', max_length=10), + ), + ] diff --git a/backend/workflows/models.py b/backend/workflows/models.py index b5ef9b6..d732220 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -80,6 +80,7 @@ class OnboardingRequest(models.Model): generated_pdf_path = models.CharField(max_length=500, blank=True) intro_pdf_path = models.CharField(max_length=500, blank=True) + preferred_language = models.CharField(max_length=10, blank=True, default='de') created_at = models.DateTimeField(auto_now_add=True) def __str__(self) -> str: @@ -181,7 +182,9 @@ class NotificationTemplate(models.Model): key = models.CharField(max_length=60, choices=TEMPLATE_CHOICES, unique=True) subject_template = models.CharField(max_length=255) + subject_template_en = models.CharField(max_length=255, blank=True) body_template = models.TextField() + body_template_en = models.TextField(blank=True) is_active = models.BooleanField(default=True) updated_at = models.DateTimeField(auto_now=True) @@ -191,6 +194,18 @@ class NotificationTemplate(models.Model): def __str__(self) -> str: return self.get_key_display() + def translated_subject_template(self, language_code: str | None = None) -> str: + lang = (language_code or get_language() or 'de').split('-')[0] + if lang == 'en' and self.subject_template_en.strip(): + return self.subject_template_en.strip() + return self.subject_template.strip() + + def translated_body_template(self, language_code: str | None = None) -> str: + lang = (language_code or get_language() or 'de').split('-')[0] + if lang == 'en' and self.body_template_en.strip(): + return self.body_template_en.strip() + return self.body_template.strip() + class NotificationRule(models.Model): EVENT_CHOICES = [ @@ -216,7 +231,9 @@ class NotificationRule(models.Model): ) template_key = models.CharField(max_length=60, blank=True) custom_subject = models.CharField(max_length=255, blank=True) + custom_subject_en = models.CharField(max_length=255, blank=True) custom_body = models.TextField(blank=True) + custom_body_en = models.TextField(blank=True) include_pdf_attachment = models.BooleanField(default=False) sort_order = models.PositiveIntegerField(default=0) @@ -227,6 +244,18 @@ class NotificationRule(models.Model): state = 'aktiv' if self.is_active else 'inaktiv' return f'{self.get_event_type_display()} | {self.name} ({state})' + def translated_custom_subject(self, language_code: str | None = None) -> str: + lang = (language_code or get_language() or 'de').split('-')[0] + if lang == 'en' and self.custom_subject_en.strip(): + return self.custom_subject_en.strip() + return self.custom_subject.strip() + + def translated_custom_body(self, language_code: str | None = None) -> str: + lang = (language_code or get_language() or 'de').split('-')[0] + if lang == 'en' and self.custom_body_en.strip(): + return self.custom_body_en.strip() + return self.custom_body.strip() + class ScheduledWelcomeEmail(models.Model): STATUS_CHOICES = [ @@ -393,6 +422,7 @@ class OffboardingRequest(models.Model): signature = models.CharField(max_length=255, blank=True, verbose_name='Unterschrift (Name)') requested_by_email = models.EmailField(verbose_name='E-Mail der anfordernden Person') requested_by_name = models.CharField(max_length=255, blank=True, verbose_name='Name der anfordernden Person') + preferred_language = models.CharField(max_length=10, blank=True, default='de') generated_pdf_path = models.CharField(max_length=500, blank=True) created_at = models.DateTimeField(auto_now_add=True) diff --git a/backend/workflows/tasks.py b/backend/workflows/tasks.py index fd963be..8bc0ffe 100644 --- a/backend/workflows/tasks.py +++ b/backend/workflows/tasks.py @@ -32,6 +32,7 @@ from .forms import ( DEFAULT_NOTIFICATION_TEMPLATES = { 'onboarding_it': { 'subject': '[Onboarding] {{ FULL_NAME }} | Anfrage von {{ REQUESTED_BY }}', + 'subject_en': '[Onboarding] {{ FULL_NAME }} | Requested by {{ REQUESTED_BY }}', 'body': ( 'Neue Onboarding-Anfrage für {{ FULL_NAME }}.\n' 'Abteilung: {{ DEPARTMENT }}\n' @@ -39,9 +40,17 @@ DEFAULT_NOTIFICATION_TEMPLATES = { 'Angefordert von: {{ REQUESTED_BY }}\n' 'Bitte IT-Setup vorbereiten.' ), + 'body_en': ( + 'New onboarding request for {{ FULL_NAME }}.\n' + 'Department: {{ DEPARTMENT }}\n' + 'Contract start: {{ CONTRACT_START }}\n' + 'Requested by: {{ REQUESTED_BY }}\n' + 'Please prepare the IT setup.' + ), }, 'onboarding_general_info': { 'subject': '[Info Onboarding] {{ FULL_NAME }} | Anfrage von {{ REQUESTED_BY }}', + 'subject_en': '[Onboarding Info] {{ FULL_NAME }} | Requested by {{ REQUESTED_BY }}', 'body': ( 'Hallo,\n\n' '{{ FULL_NAME }} wird onboarded.\n' @@ -49,9 +58,17 @@ DEFAULT_NOTIFICATION_TEMPLATES = { 'Vertragsbeginn: {{ CONTRACT_START }}\n' 'Angefordert von: {{ REQUESTED_BY }}\n' ), + 'body_en': ( + 'Hello,\n\n' + '{{ FULL_NAME }} is being onboarded.\n' + 'Department: {{ DEPARTMENT }}\n' + 'Contract start: {{ CONTRACT_START }}\n' + 'Requested by: {{ REQUESTED_BY }}\n' + ), }, 'onboarding_business_card': { 'subject': '[Visitenkarte] {{ FULL_NAME }} | Anfrage von {{ REQUESTED_BY }}', + 'subject_en': '[Business Card] {{ FULL_NAME }} | Requested by {{ REQUESTED_BY }}', 'body': ( 'Hallo,\n\n' 'bitte Visitenkarten erstellen:\n' @@ -61,9 +78,19 @@ DEFAULT_NOTIFICATION_TEMPLATES = { 'Telefon: {{ BUSINESS_CARD_PHONE }}\n' 'Angefordert von: {{ REQUESTED_BY }}\n' ), + 'body_en': ( + 'Hello,\n\n' + 'please create business cards:\n' + 'Name: {{ BUSINESS_CARD_NAME }}\n' + 'Title: {{ BUSINESS_CARD_TITLE }}\n' + 'Email: {{ BUSINESS_CARD_EMAIL }}\n' + 'Phone: {{ BUSINESS_CARD_PHONE }}\n' + 'Requested by: {{ REQUESTED_BY }}\n' + ), }, 'onboarding_hr_works': { 'subject': '[HR Works] {{ FULL_NAME }} | Anfrage von {{ REQUESTED_BY }}', + 'subject_en': '[HR Works] {{ FULL_NAME }} | Requested by {{ REQUESTED_BY }}', 'body': ( 'Hello Stefanie,\n\n' 'Es ist wieder soweit. Zuwachs!\n\n' @@ -77,9 +104,23 @@ DEFAULT_NOTIFICATION_TEMPLATES = { 'Vielen Dank und schöne Grüße,\n' 'Die IT.' ), + 'body_en': ( + 'Hello Stefanie,\n\n' + 'we have a new team member joining.\n\n' + 'Could you please create an HR Works account with the following details:\n\n' + 'Name: {{ VORNAME }} {{ NACHNAME }}\n' + 'Department: {{ DEPARTMENT }}\n' + 'Contract start: {{ CONTRACT_START }}\n' + 'Email address: {{ EMAIL }}\n\n' + '{% if PDF_LINK %}You will find the employee PDF here in about 2 minutes: {{ PDF_LINK }}\n\n{% endif %}' + 'If you need any other information, please contact it@tub.co.\n\n' + 'Thank you and best regards,\n' + 'IT' + ), }, 'onboarding_key': { 'subject': '[Schlüssel] {{ FULL_NAME }} | Anfrage von {{ REQUESTED_BY }}', + 'subject_en': '[Key] {{ FULL_NAME }} | Requested by {{ REQUESTED_BY }}', 'body': ( 'Hallo,\n\n' 'bitte Schlüssel vorbereiten für:\n' @@ -88,9 +129,18 @@ DEFAULT_NOTIFICATION_TEMPLATES = { 'Vertragsbeginn: {{ CONTRACT_START }}\n' 'Angefordert von: {{ REQUESTED_BY }}\n' ), + 'body_en': ( + 'Hello,\n\n' + 'please prepare keys for:\n' + 'Name: {{ FULL_NAME }}\n' + 'Department: {{ DEPARTMENT }}\n' + 'Contract start: {{ CONTRACT_START }}\n' + 'Requested by: {{ REQUESTED_BY }}\n' + ), }, 'onboarding_reference': { 'subject': '[Referenz Onboarding] {{ FULL_NAME }} | Ihre Anfrage', + 'subject_en': '[Onboarding Reference] {{ FULL_NAME }} | Your Request', 'body': ( 'Diese E-Mail dient als Referenz für Ihre Onboarding-Anfrage.\n' 'Name: {{ FULL_NAME }}\n' @@ -98,9 +148,17 @@ DEFAULT_NOTIFICATION_TEMPLATES = { 'Vertragsbeginn: {{ CONTRACT_START }}\n' 'Angefordert von: {{ REQUESTED_BY }}\n' ), + 'body_en': ( + 'This email is your reference copy for the onboarding request.\n' + 'Name: {{ FULL_NAME }}\n' + 'Department: {{ DEPARTMENT }}\n' + 'Contract start: {{ CONTRACT_START }}\n' + 'Requested by: {{ REQUESTED_BY }}\n' + ), }, 'onboarding_welcome': { 'subject': 'Willkommen bei TUB/CO, {{ VORNAME }}', + 'subject_en': 'Welcome to TUB/CO, {{ VORNAME }}', 'body': ( 'Hallo {{ FULL_NAME }},\n\n' 'herzlich willkommen bei TUB/CO.\n' @@ -111,9 +169,20 @@ DEFAULT_NOTIFICATION_TEMPLATES = { 'Viele Grüße\n' 'TUB/CO IT' ), + 'body_en': ( + 'Hello {{ FULL_NAME }},\n\n' + 'welcome to TUB/CO.\n' + 'We are very happy that you will join our {{ DEPARTMENT }} team starting on {{ CONTRACT_START }}.\n\n' + 'Your work email address is: {{ EMAIL }}.\n' + 'You will find your onboarding documents attached as a PDF.\n\n' + 'If you have any questions, feel free to contact us anytime.\n\n' + 'Best regards,\n' + 'TUB/CO IT' + ), }, 'offboarding_it': { 'subject': '[Offboarding] {{ FULL_NAME }} | Anfrage von {{ REQUESTED_BY }}', + 'subject_en': '[Offboarding] {{ FULL_NAME }} | Requested by {{ REQUESTED_BY }}', 'body': ( 'Neue Offboarding-Anfrage für {{ FULL_NAME }}.\n' 'Abteilung: {{ DEPARTMENT }}\n' @@ -121,25 +190,45 @@ DEFAULT_NOTIFICATION_TEMPLATES = { 'Angefordert von: {{ REQUESTED_BY }}\n' 'Bitte IT-Offboarding durchführen.' ), + 'body_en': ( + 'New offboarding request for {{ FULL_NAME }}.\n' + 'Department: {{ DEPARTMENT }}\n' + 'Last working day: {{ LAST_WORKING_DAY }}\n' + 'Requested by: {{ REQUESTED_BY }}\n' + 'Please complete the IT offboarding.' + ), }, 'offboarding_general_info': { 'subject': '[Info Offboarding] {{ FULL_NAME }} | Anfrage von {{ REQUESTED_BY }}', + 'subject_en': '[Offboarding Info] {{ FULL_NAME }} | Requested by {{ REQUESTED_BY }}', 'body': ( 'Neue Offboarding-Anfrage für {{ FULL_NAME }}.\n' 'Abteilung: {{ DEPARTMENT }}\n' 'Letzter Arbeitstag: {{ LAST_WORKING_DAY }}\n' 'Angefordert von: {{ REQUESTED_BY }}\n' ), + 'body_en': ( + 'New offboarding request for {{ FULL_NAME }}.\n' + 'Department: {{ DEPARTMENT }}\n' + 'Last working day: {{ LAST_WORKING_DAY }}\n' + 'Requested by: {{ REQUESTED_BY }}\n' + ), }, 'offboarding_hr_works_disable': { 'subject': '[HR Works Deaktivierung] {{ FULL_NAME }} | Anfrage von {{ REQUESTED_BY }}', + 'subject_en': '[HR Works Disable] {{ FULL_NAME }} | Requested by {{ REQUESTED_BY }}', 'body': ( 'Bitte HR Works Zugriff deaktivieren für {{ FULL_NAME }} ({{ EMAIL }}) zum {{ LAST_WORKING_DAY }}.\n' 'Angefordert von: {{ REQUESTED_BY }}\n' ), + 'body_en': ( + 'Please disable HR Works access for {{ FULL_NAME }} ({{ EMAIL }}) effective {{ LAST_WORKING_DAY }}.\n' + 'Requested by: {{ REQUESTED_BY }}\n' + ), }, 'offboarding_reference': { 'subject': '[Referenz Offboarding] {{ FULL_NAME }} | Ihre Anfrage', + 'subject_en': '[Offboarding Reference] {{ FULL_NAME }} | Your Request', 'body': ( 'Diese E-Mail dient als Referenz für Ihre Offboarding-Anfrage.\n' 'Name: {{ FULL_NAME }}\n' @@ -147,6 +236,13 @@ DEFAULT_NOTIFICATION_TEMPLATES = { 'Letzter Arbeitstag: {{ LAST_WORKING_DAY }}\n' 'Angefordert von: {{ REQUESTED_BY }}\n' ), + 'body_en': ( + 'This email is your reference copy for the offboarding request.\n' + 'Name: {{ FULL_NAME }}\n' + 'Department: {{ DEPARTMENT }}\n' + 'Last working day: {{ LAST_WORKING_DAY }}\n' + 'Requested by: {{ REQUESTED_BY }}\n' + ), }, } @@ -419,15 +515,16 @@ def _send_workflow_email( ) -def _render_notification_template(template_key: str, context: dict) -> tuple[str, str]: +def _render_notification_template(template_key: str, context: dict, language_code: str | None = None) -> tuple[str, str]: + lang = (language_code or 'de').split('-')[0] db_template = NotificationTemplate.objects.filter(key=template_key, is_active=True).first() if db_template: - subject_template = db_template.subject_template - body_template = db_template.body_template + subject_template = db_template.translated_subject_template(lang) + body_template = db_template.translated_body_template(lang) else: fallback = DEFAULT_NOTIFICATION_TEMPLATES[template_key] - subject_template = fallback['subject'] - body_template = fallback['body'] + subject_template = fallback.get(f'subject_{lang}', '') or fallback['subject'] + body_template = fallback.get(f'body_{lang}', '') or fallback['body'] subject = Template(subject_template).render(context).strip() body = Template(body_template).render(context).strip() @@ -475,6 +572,7 @@ def _apply_notification_rules( context: dict, pdf_path: Path | None = None, ) -> None: + language_code = (getattr(request_obj, 'preferred_language', '') or 'de').split('-')[0] rules = NotificationRule.objects.filter(event_type=event_type, is_active=True).order_by('sort_order', 'id') for rule in rules: if not _rule_matches(rule, request_obj): @@ -494,11 +592,12 @@ def _apply_notification_rules( context=context, to=recipients, attachments=attachments, + language_code=language_code, ) continue - subject = (rule.custom_subject or '').strip() - body = (rule.custom_body or '').strip() + subject = rule.translated_custom_subject(language_code) + body = rule.translated_custom_body(language_code) if not subject and not body: continue @@ -547,8 +646,9 @@ def _send_templated_email( context: dict, attachments: list[Path] | None = None, from_email: str | None = None, + language_code: str | None = None, ) -> None: - subject, body = _render_notification_template(template_key, context) + subject, body = _render_notification_template(template_key, context, language_code=language_code) _send_workflow_email(subject=subject, body=body, to=to, attachments=attachments, from_email=from_email) diff --git a/backend/workflows/templates/workflows/developer_handbook.html b/backend/workflows/templates/workflows/developer_handbook.html index dfe1d5f..2e222f4 100644 --- a/backend/workflows/templates/workflows/developer_handbook.html +++ b/backend/workflows/templates/workflows/developer_handbook.html @@ -126,6 +126,8 @@ docker compose exec -T web django-admin compilemessages
  • Do not use custom ad hoc .mo compilation anymore.
  • Phase 1 bilingual support covers fixed UI.
  • Phase 2 covers builder-driven labels and checklist items with explicit German and English fields.
  • +
  • Email phase covers NotificationTemplate, NotificationRule, and the welcome-email settings UI with explicit DE/EN subject/body fields.
  • +
  • Request records now persist preferred_language so workflow emails can render in the submitter's active UI language with German fallback.
  • 7) PDF Pipeline

    @@ -150,6 +152,8 @@ docker compose exec -T web django-admin compilemessages diff --git a/backend/workflows/templates/workflows/integrations_setup.html b/backend/workflows/templates/workflows/integrations_setup.html index 299c355..e6fd108 100644 --- a/backend/workflows/templates/workflows/integrations_setup.html +++ b/backend/workflows/templates/workflows/integrations_setup.html @@ -195,13 +195,21 @@

    {{ tpl.get_key_display }} ({{ tpl.key }})

    - +
    - +
    +
    + + +
    +
    + + +
    {% endfor %} @@ -269,6 +277,14 @@ +
    + + +
    +
    + + +
    @@ -332,6 +348,14 @@
    +
    + + +
    +
    + + +
    diff --git a/backend/workflows/templates/workflows/project_wiki.html b/backend/workflows/templates/workflows/project_wiki.html index 58f3678..509195f 100644 --- a/backend/workflows/templates/workflows/project_wiki.html +++ b/backend/workflows/templates/workflows/project_wiki.html @@ -166,8 +166,10 @@
  • Current scope: the core user interface supports German and English switching for the main fixed UI pages.
  • Covered now: login, home, requests dashboard, onboarding form shell, offboarding form shell, and common status/messages in views.
  • Phase 2 added: dynamic Form Builder option labels, field label/help-text overrides, and intro-builder checklist item labels now support German and English values.
  • +
  • Email phase added: admin-managed notification template subject/body content, notification rule custom subject/body content, and welcome email subject/body content now support explicit DE/EN values.
  • +
  • Runtime selection: onboarding and offboarding requests store the UI language at submission time, and notification delivery uses that language with German fallback.
  • Editing path: these DE/EN values are maintained directly in the frontend builder pages, not only in Django admin.
  • -
  • Not fully bilingual yet: admin-configured email templates and several generated PDF/business text blocks still remain primarily single-language.
  • +
  • Not fully bilingual yet: several generated PDF/business text blocks still remain primarily single-language.
  • Implementation: Django i18n with locale middleware, translation catalogs, and a DE/EN language switch in the main UI.
  • diff --git a/backend/workflows/templates/workflows/welcome_emails.html b/backend/workflows/templates/workflows/welcome_emails.html index 816860d..e124560 100644 --- a/backend/workflows/templates/workflows/welcome_emails.html +++ b/backend/workflows/templates/workflows/welcome_emails.html @@ -68,13 +68,21 @@
    - +
    - +
    +
    + + +
    +
    + + +
    diff --git a/backend/workflows/views.py b/backend/workflows/views.py index 79b2b09..4e0a7ab 100644 --- a/backend/workflows/views.py +++ b/backend/workflows/views.py @@ -393,7 +393,8 @@ def onboarding_create(request): if form.is_valid(): obj = form.save() obj.onboarded_by_name = _display_user_name(request.user) - obj.save(update_fields=['onboarded_by_name']) + obj.preferred_language = (get_language() or 'de').split('-')[0] + obj.save(update_fields=['onboarded_by_name', 'preferred_language']) process_onboarding_request.delay(obj.id) return redirect(f"/onboarding/new/?saved=1&id={obj.id}") else: @@ -565,6 +566,7 @@ def offboarding_create(request): else: obj.requested_by_email = settings.DEFAULT_FROM_EMAIL obj.requested_by_name = _display_user_name(request.user) + obj.preferred_language = (get_language() or 'de').split('-')[0] obj.save() process_offboarding_request.delay(obj.id) return redirect(f"/offboarding/new/?saved=1&id={obj.id}") @@ -895,8 +897,12 @@ def welcome_emails_page(request): default_welcome = DEFAULT_NOTIFICATION_TEMPLATES.get('onboarding_welcome', {}) default_subject = (default_welcome.get('subject') or 'Willkommen bei TUB/CO, {{ FULL_NAME }}').strip() default_body = (default_welcome.get('body') or 'Hallo {{ FULL_NAME }}, willkommen bei TUB/CO.').strip() + default_subject_en = (default_welcome.get('subject_en') or 'Welcome to TUB/CO, {{ FULL_NAME }}').strip() + default_body_en = (default_welcome.get('body_en') or 'Hello {{ FULL_NAME }}, welcome to TUB/CO.').strip() subject_value = (welcome_template.subject_template if welcome_template else '').strip() or default_subject body_value = (welcome_template.body_template if welcome_template else '').strip() or default_body + subject_value_en = (welcome_template.subject_template_en if welcome_template else '').strip() or default_subject_en + body_value_en = (welcome_template.body_template_en if welcome_template else '').strip() or default_body_en return render( request, 'workflows/welcome_emails.html', @@ -906,6 +912,8 @@ def welcome_emails_page(request): 'welcome_template': welcome_template, 'welcome_subject_value': subject_value, 'welcome_body_value': body_value, + 'welcome_subject_value_en': subject_value_en, + 'welcome_body_value_en': body_value_en, 'welcome_keywords': ['{{ FULL_NAME }}', '{{ VORNAME }}', '{{ NACHNAME }}', '{{ DEPARTMENT }}', '{{ CONTRACT_START }}', '{{ EMAIL }}', '{{ REQUESTED_BY }}'], }, ) @@ -950,17 +958,25 @@ def save_welcome_email_settings(request): subject = request.POST.get('welcome_subject') body = request.POST.get('welcome_body') - if subject is not None or body is not None: + subject_en = request.POST.get('welcome_subject_en') + body_en = request.POST.get('welcome_body_en') + if subject is not None or body is not None or subject_en is not None or body_en is not None: default_welcome = DEFAULT_NOTIFICATION_TEMPLATES.get('onboarding_welcome', {}) default_subject = (default_welcome.get('subject') or 'Willkommen bei TUB/CO, {{ FULL_NAME }}').strip() default_body = (default_welcome.get('body') or 'Hallo {{ FULL_NAME }}, willkommen bei TUB/CO.').strip() + default_subject_en = (default_welcome.get('subject_en') or 'Welcome to TUB/CO, {{ FULL_NAME }}').strip() + default_body_en = (default_welcome.get('body_en') or 'Hello {{ FULL_NAME }}, welcome to TUB/CO.').strip() subject_clean = (subject or '').strip() or default_subject body_clean = (body or '').strip() or default_body + subject_clean_en = (subject_en or '').strip() or default_subject_en + body_clean_en = (body_en or '').strip() or default_body_en template, _ = NotificationTemplate.objects.get_or_create( key='onboarding_welcome', defaults={ 'subject_template': subject_clean, 'body_template': body_clean, + 'subject_template_en': subject_clean_en, + 'body_template_en': body_clean_en, 'is_active': True, }, ) @@ -971,6 +987,12 @@ def save_welcome_email_settings(request): if template.body_template != body_clean: template.body_template = body_clean changes.append('body_template') + if template.subject_template_en != subject_clean_en: + template.subject_template_en = subject_clean_en + changes.append('subject_template_en') + if template.body_template_en != body_clean_en: + template.body_template_en = body_clean_en + changes.append('body_template_en') if not template.is_active: template.is_active = True changes.append('is_active') @@ -1392,17 +1414,23 @@ def save_email_routing_settings(request): for key in known_keys: subject = request.POST.get(f'subject_{key}') body = request.POST.get(f'body_{key}') - if subject is None and body is None: + subject_en = request.POST.get(f'subject_en_{key}') + body_en = request.POST.get(f'body_en_{key}') + if subject is None and body is None and subject_en is None and body_en is None: continue subject = (subject or '').strip() body = (body or '').strip() - if not subject and not body: + subject_en = (subject_en or '').strip() + body_en = (body_en or '').strip() + if not subject and not body and not subject_en and not body_en: continue obj, _ = NotificationTemplate.objects.get_or_create( key=key, defaults={ 'subject_template': subject or f'[{key}]', 'body_template': body or '-', + 'subject_template_en': subject_en, + 'body_template_en': body_en, 'is_active': True, }, ) @@ -1413,6 +1441,15 @@ def save_email_routing_settings(request): if body and obj.body_template != body: obj.body_template = body changed.append('body_template') + if obj.subject_template_en != subject_en: + obj.subject_template_en = subject_en + changed.append('subject_template_en') + if obj.body_template_en != body_en: + obj.body_template_en = body_en + changed.append('body_template_en') + if not obj.is_active: + obj.is_active = True + changed.append('is_active') if changed: obj.save(update_fields=changed) @@ -1446,6 +1483,8 @@ def save_notification_rules(request): rule.template_key = request.POST.get(f'template_key_{rule.id}', '').strip() rule.custom_subject = request.POST.get(f'custom_subject_{rule.id}', '').strip() rule.custom_body = request.POST.get(f'custom_body_{rule.id}', '').strip() + rule.custom_subject_en = request.POST.get(f'custom_subject_en_{rule.id}', '').strip() + rule.custom_body_en = request.POST.get(f'custom_body_en_{rule.id}', '').strip() rule.include_pdf_attachment = request.POST.get(f'include_pdf_{rule.id}') == 'on' rule.is_active = request.POST.get(f'active_{rule.id}') == 'on' rule.sort_order = position @@ -1470,6 +1509,8 @@ def save_notification_rules(request): template_key=request.POST.get('new_template_key', '').strip(), custom_subject=request.POST.get('new_custom_subject', '').strip(), custom_body=request.POST.get('new_custom_body', '').strip(), + custom_subject_en=request.POST.get('new_custom_subject_en', '').strip(), + custom_body_en=request.POST.get('new_custom_body_en', '').strip(), include_pdf_attachment=request.POST.get('new_include_pdf') == 'on', is_active=True, sort_order=NotificationRule.objects.filter(event_type=new_event).count() + 1,