snapshot: preserve bilingual email template phase
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -126,6 +126,8 @@ docker compose exec -T web django-admin compilemessages</code></pre>
|
||||
<li>Do not use custom ad hoc <code>.mo</code> compilation anymore.</li>
|
||||
<li>Phase 1 bilingual support covers fixed UI.</li>
|
||||
<li>Phase 2 covers builder-driven labels and checklist items with explicit German and English fields.</li>
|
||||
<li>Email phase covers <code>NotificationTemplate</code>, <code>NotificationRule</code>, and the welcome-email settings UI with explicit DE/EN subject/body fields.</li>
|
||||
<li>Request records now persist <code>preferred_language</code> so workflow emails can render in the submitter's active UI language with German fallback.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="pdf">7) PDF Pipeline</h2>
|
||||
@@ -150,6 +152,8 @@ docker compose exec -T web django-admin compilemessages</code></pre>
|
||||
<ul>
|
||||
<li>Notification defaults are defined in <code>DEFAULT_NOTIFICATION_TEMPLATES</code> in <code>tasks.py</code>.</li>
|
||||
<li>Admin-configured overrides live in <code>NotificationTemplate</code> and <code>NotificationRule</code>.</li>
|
||||
<li>Both models now support explicit DE/EN content fields. Prefer filling both languages in the frontend integrations UI instead of embedding mixed-language copy into a single template.</li>
|
||||
<li>Welcome-email configuration under Admin Apps has the same DE/EN split and follows the same runtime language selection.</li>
|
||||
<li>Mail sending uses Celery tasks and supports test mode redirection.</li>
|
||||
<li>MailHog is the local verification path when test mode is active.</li>
|
||||
</ul>
|
||||
|
||||
@@ -195,13 +195,21 @@
|
||||
<p class="template-title">{{ tpl.get_key_display }} ({{ tpl.key }})</p>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<label for="subject_{{ tpl.key }}">Subject</label>
|
||||
<label for="subject_{{ tpl.key }}">Subject (DE)</label>
|
||||
<input id="subject_{{ tpl.key }}" name="subject_{{ tpl.key }}" value="{{ tpl.subject_template }}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="body_{{ tpl.key }}">Body</label>
|
||||
<label for="body_{{ tpl.key }}">Body (DE)</label>
|
||||
<textarea id="body_{{ tpl.key }}" name="body_{{ tpl.key }}">{{ tpl.body_template }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="subject_en_{{ tpl.key }}">Subject (EN)</label>
|
||||
<input id="subject_en_{{ tpl.key }}" name="subject_en_{{ tpl.key }}" value="{{ tpl.subject_template_en }}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="body_en_{{ tpl.key }}">Body (EN)</label>
|
||||
<textarea id="body_en_{{ tpl.key }}" name="body_en_{{ tpl.key }}">{{ tpl.body_template_en }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -269,6 +277,14 @@
|
||||
<label for="custom_body_{{ rule.id }}">Custom Body (optional)</label>
|
||||
<textarea id="custom_body_{{ rule.id }}" name="custom_body_{{ rule.id }}">{{ rule.custom_body }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom_subject_en_{{ rule.id }}">Custom Subject (EN, optional)</label>
|
||||
<input id="custom_subject_en_{{ rule.id }}" name="custom_subject_en_{{ rule.id }}" value="{{ rule.custom_subject_en }}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom_body_en_{{ rule.id }}">Custom Body (EN, optional)</label>
|
||||
<textarea id="custom_body_en_{{ rule.id }}" name="custom_body_en_{{ rule.id }}">{{ rule.custom_body_en }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="check-row">
|
||||
<label><input type="checkbox" name="active_{{ rule.id }}" {% if rule.is_active %}checked{% endif %} /> Aktiv</label>
|
||||
@@ -332,6 +348,14 @@
|
||||
<label for="new_custom_body">Custom Body (optional)</label>
|
||||
<textarea id="new_custom_body" name="new_custom_body"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="new_custom_subject_en">Custom Subject (EN, optional)</label>
|
||||
<input id="new_custom_subject_en" name="new_custom_subject_en" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="new_custom_body_en">Custom Body (EN, optional)</label>
|
||||
<textarea id="new_custom_body_en" name="new_custom_body_en"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="check-row">
|
||||
<label><input type="checkbox" name="new_include_pdf" /> PDF anhängen</label>
|
||||
|
||||
@@ -166,8 +166,10 @@
|
||||
<li><strong>Current scope:</strong> the core user interface supports German and English switching for the main fixed UI pages.</li>
|
||||
<li><strong>Covered now:</strong> login, home, requests dashboard, onboarding form shell, offboarding form shell, and common status/messages in views.</li>
|
||||
<li><strong>Phase 2 added:</strong> dynamic Form Builder option labels, field label/help-text overrides, and intro-builder checklist item labels now support German and English values.</li>
|
||||
<li><strong>Email phase added:</strong> 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.</li>
|
||||
<li><strong>Runtime selection:</strong> onboarding and offboarding requests store the UI language at submission time, and notification delivery uses that language with German fallback.</li>
|
||||
<li><strong>Editing path:</strong> these DE/EN values are maintained directly in the frontend builder pages, not only in Django admin.</li>
|
||||
<li><strong>Not fully bilingual yet:</strong> admin-configured email templates and several generated PDF/business text blocks still remain primarily single-language.</li>
|
||||
<li><strong>Not fully bilingual yet:</strong> several generated PDF/business text blocks still remain primarily single-language.</li>
|
||||
<li><strong>Implementation:</strong> Django i18n with locale middleware, translation catalogs, and a DE/EN language switch in the main UI.</li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -68,13 +68,21 @@
|
||||
<input id="welcome_sender_email" name="welcome_sender_email" type="email" value="{{ workflow_config.welcome_sender_email }}" placeholder="Leer = System-Absender" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="welcome_subject">Welcome Subject</label>
|
||||
<label for="welcome_subject">Welcome Subject (DE)</label>
|
||||
<input id="welcome_subject" name="welcome_subject" value="{{ welcome_subject_value }}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="welcome_body">Welcome Text</label>
|
||||
<label for="welcome_body">Welcome Text (DE)</label>
|
||||
<textarea id="welcome_body" name="welcome_body">{{ welcome_body_value }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="welcome_subject_en">Welcome Subject (EN)</label>
|
||||
<input id="welcome_subject_en" name="welcome_subject_en" value="{{ welcome_subject_value_en }}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="welcome_body_en">Welcome Text (EN)</label>
|
||||
<textarea id="welcome_body_en" name="welcome_body_en">{{ welcome_body_value_en }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="check-row">
|
||||
<label><input type="checkbox" name="welcome_include_pdf" {% if workflow_config.welcome_include_pdf %}checked{% endif %} /> Onboarding-PDF anhängen</label>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user