Skip to content

Commit 6a67719

Browse files
feat: make notification emails translatable (#36775)
* feat: make notification emails translatable * fix: fixed failing tests * fix: fixed xss quality check
1 parent 76f872c commit 6a67719

14 files changed

Lines changed: 144 additions & 64 deletions

File tree

openedx/core/djangoapps/notifications/email/tasks.py

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from celery import shared_task
66
from celery.utils.log import get_task_logger
77
from django.contrib.auth import get_user_model
8+
from django.utils.translation import gettext as _, override as translation_override
89
from edx_ace import ace
910
from edx_ace.recipient import Recipient
1011
from edx_django_utils.monitoring import set_code_owner_attribute
@@ -24,10 +25,11 @@
2425
create_email_template_context,
2526
filter_notification_with_email_enabled_preferences,
2627
get_course_info,
28+
get_language_preference_for_users,
2729
get_start_end_date,
2830
get_text_for_notification_type,
2931
get_unique_course_ids,
30-
is_email_notification_flag_enabled
32+
is_email_notification_flag_enabled,
3133
)
3234

3335

@@ -74,7 +76,7 @@ def get_user_preferences_for_courses(course_ids, user):
7476
return new_preferences
7577

7678

77-
def send_digest_email_to_user(user, cadence_type, start_date, end_date, course_language='en', courses_data=None):
79+
def send_digest_email_to_user(user, cadence_type, start_date, end_date, user_language='en', courses_data=None):
7880
"""
7981
Send [cadence_type] email to user.
8082
Cadence Type can be EmailCadence.DAILY or EmailCadence.WEEKLY
@@ -95,24 +97,26 @@ def send_digest_email_to_user(user, cadence_type, start_date, end_date, course_l
9597
if not notifications:
9698
logger.info(f'<Email Cadence> No notification for {user.username} ==Temp Log==')
9799
return
98-
course_ids = get_unique_course_ids(notifications)
99-
preferences = get_user_preferences_for_courses(course_ids, user)
100-
notifications = filter_notification_with_email_enabled_preferences(notifications, preferences, cadence_type)
101-
if not notifications:
102-
logger.info(f'<Email Cadence> No filtered notification for {user.username} ==Temp Log==')
103-
return
104-
apps_dict = create_app_notifications_dict(notifications)
105-
message_context = create_email_digest_context(apps_dict, user.username, start_date, end_date,
106-
cadence_type, courses_data=courses_data)
107-
recipient = Recipient(user.id, user.email)
108-
message = EmailNotificationMessageType(
109-
app_label="notifications", name="email_digest"
110-
).personalize(recipient, course_language, message_context)
111-
message = add_headers_to_email_message(message, message_context)
112-
message.options['skip_disable_user_policy'] = True
113-
ace.send(message)
114-
send_user_email_digest_sent_event(user, cadence_type, notifications, message_context)
115-
logger.info(f'<Email Cadence> Email sent to {user.username} ==Temp Log==')
100+
101+
with translation_override(user_language):
102+
course_ids = get_unique_course_ids(notifications)
103+
preferences = get_user_preferences_for_courses(course_ids, user)
104+
notifications = filter_notification_with_email_enabled_preferences(notifications, preferences, cadence_type)
105+
if not notifications:
106+
logger.info(f'<Email Cadence> No filtered notification for {user.username} ==Temp Log==')
107+
return
108+
apps_dict = create_app_notifications_dict(notifications)
109+
message_context = create_email_digest_context(apps_dict, user.username, start_date, end_date,
110+
cadence_type, courses_data=courses_data)
111+
recipient = Recipient(user.id, user.email)
112+
message = EmailNotificationMessageType(
113+
app_label="notifications", name="email_digest"
114+
).personalize(recipient, user_language, message_context)
115+
message = add_headers_to_email_message(message, message_context)
116+
message.options['skip_disable_user_policy'] = True
117+
ace.send(message)
118+
send_user_email_digest_sent_event(user, cadence_type, notifications, message_context)
119+
logger.info(f'<Email Cadence> Email sent to {user.username} ==Temp Log==')
116120

117121

118122
@shared_task(ignore_result=True)
@@ -123,11 +127,14 @@ def send_digest_email_to_all_users(cadence_type):
123127
"""
124128
logger.info(f'<Email Cadence> Sending cadence email of type {cadence_type}')
125129
users = get_audience_for_cadence_email(cadence_type)
130+
language_prefs = get_language_preference_for_users([user.id for user in users])
126131
courses_data = {}
127132
start_date, end_date = get_start_end_date(cadence_type)
128133
logger.info(f'<Email Cadence> Email Cadence Audience {len(users)}')
129134
for user in users:
130-
send_digest_email_to_user(user, cadence_type, start_date, end_date, courses_data=courses_data)
135+
user_language = language_prefs.get(user.id, 'en')
136+
send_digest_email_to_user(user, cadence_type, start_date, end_date, user_language=user_language,
137+
courses_data=courses_data)
131138

132139

133140
def send_immediate_cadence_email(email_notification_mapping, course_key):
@@ -141,6 +148,7 @@ def send_immediate_cadence_email(email_notification_mapping, course_key):
141148
return
142149
user_list = email_notification_mapping.keys()
143150
users = User.objects.filter(id__in=user_list)
151+
language_prefs = get_language_preference_for_users(user_list)
144152
course_name = get_course_info(course_key).get("name", course_key)
145153
for user in users.iterator(chunk_size=100):
146154
if not user.has_usable_password():
@@ -153,21 +161,25 @@ def send_immediate_cadence_email(email_notification_mapping, course_key):
153161
if not notification:
154162
logger.info(f'<Immediate Email> No notification for {user.username}')
155163
continue
156-
soup = BeautifulSoup(notification.content, "html.parser")
157-
title = "New Course Update" if notification.notification_type == "course_updates" else soup.get_text()
158-
message_context = create_email_template_context(user.username)
159-
message_context.update({
160-
"course_id": course_key,
161-
"course_name": course_name,
162-
"content_url": notification.content_url,
163-
"content_title": title,
164-
"footer_email_reason": "You are receiving this email because you are enrolled in the edX course "
165-
f"{course_name}",
166-
"content": notification.content_context.get("email_content", notification.content),
167-
"view_text": get_text_for_notification_type(notification.notification_type),
168-
})
169-
message = EmailNotificationMessageType(
170-
app_label="notifications", name="immediate_email"
171-
).personalize(Recipient(user.id, user.email), 'en', message_context)
172-
message = add_headers_to_email_message(message, message_context)
173-
ace.send(message)
164+
165+
language = language_prefs.get(user.id, 'en')
166+
with translation_override(language):
167+
soup = BeautifulSoup(notification.content, "html.parser")
168+
title = _("New Course Update") if notification.notification_type == "course_updates" else soup.get_text()
169+
message_context = create_email_template_context(user.username)
170+
message_context.update({
171+
"course_id": course_key,
172+
"course_name": course_name,
173+
"content_url": notification.content_url,
174+
"content_title": title,
175+
"footer_email_reason": _(
176+
"You are receiving this email because you are enrolled in the edX course "
177+
) + str(course_name),
178+
"content": notification.content_context.get("email_content", notification.content),
179+
"view_text": get_text_for_notification_type(notification.notification_type),
180+
})
181+
message = EmailNotificationMessageType(
182+
app_label="notifications", name="immediate_email"
183+
).personalize(Recipient(user.id, user.email), language, message_context)
184+
message = add_headers_to_email_message(message, message_context)
185+
ace.send(message)

openedx/core/djangoapps/notifications/email/tests/test_utils.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,19 +147,21 @@ def test_email_digest_context(self, digest_frequency):
147147
context = create_email_digest_context(**params)
148148
expected_start_date = 'Sunday, Mar 24' if digest_frequency == 'Daily' else 'Monday, Mar 18'
149149
expected_digest_updates = [
150-
{'title': 'Total Notifications', 'count': 2},
151-
{'title': 'Discussion', 'count': 1},
152-
{'title': 'Updates', 'count': 1},
150+
{'title': 'Total Notifications', 'translated_title': 'Total Notifications', 'count': 2},
151+
{'title': 'Discussion', 'translated_title': 'Discussion', 'count': 1},
152+
{'title': 'Updates', 'translated_title': 'Updates', 'count': 1},
153153
]
154154
expected_email_content = [
155155
{
156156
'title': 'Discussion', 'help_text': '', 'help_text_url': '',
157+
'translated_title': 'Discussion',
157158
'notifications': [discussion_notification],
158159
'total': 1, 'show_remaining_count': False, 'remaining_count': 0,
159160
'url': 'http://learner-home-mfe/?showNotifications=true&app=discussion'
160161
},
161162
{
162163
'title': 'Updates', 'help_text': '', 'help_text_url': '',
164+
'translated_title': 'Updates',
163165
'notifications': [update_notification],
164166
'total': 1, 'show_remaining_count': False, 'remaining_count': 0,
165167
'url': 'http://learner-home-mfe/?showNotifications=true&app=updates'

openedx/core/djangoapps/notifications/email/utils.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
from django.conf import settings
99
from django.contrib.auth import get_user_model
1010
from django.shortcuts import get_object_or_404
11+
from django.utils.translation import gettext as _
1112
from pytz import utc
1213
from waffle import get_waffle_flag_model # pylint: disable=invalid-django-waffle-import
1314

1415
from common.djangoapps.student.models import CourseEnrollment
1516
from lms.djangoapps.branding.api import get_logo_url_for_email
1617
from lms.djangoapps.discussion.notification_prefs.views import UsernameCipher
18+
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
1719
from openedx.core.djangoapps.notifications.base_notification import COURSE_NOTIFICATION_APPS, COURSE_NOTIFICATION_TYPES
1820
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_EMAIL_NOTIFICATIONS
1921
from openedx.core.djangoapps.notifications.email import ONE_CLICK_EMAIL_UNSUB_KEY
@@ -28,6 +30,7 @@
2830

2931
from .notification_icons import NotificationTypeIcons
3032

33+
3134
User = get_user_model()
3235

3336

@@ -119,12 +122,14 @@ def create_email_digest_context(app_notifications_dict, username, start_date, en
119122
end_date_str = create_datetime_string(end_date if end_date else start_date)
120123
email_digest_updates = [{
121124
'title': 'Total Notifications',
125+
'translated_title': _('Total Notifications'),
122126
'count': sum(value['count'] for value in app_notifications_dict.values())
123127
}]
124128
email_digest_updates.extend([
125129
{
126130
'title': value['title'],
127131
'count': value['count'],
132+
'translated_title': value.get('translated_title', value['title']),
128133
}
129134
for key, value in app_notifications_dict.items()
130135
])
@@ -135,6 +140,7 @@ def create_email_digest_context(app_notifications_dict, username, start_date, en
135140
total = value['count']
136141
app_content = {
137142
'title': value['title'],
143+
'translated_title': value.get('translated_title', value['title']),
138144
'help_text': value.get('help_text', ''),
139145
'help_text_url': value.get('help_text_url', ''),
140146
'notifications': add_additional_attributes_to_notifications(
@@ -200,7 +206,7 @@ def get_time_ago(datetime_obj):
200206
current_date = utc.localize(datetime.datetime.today())
201207
days_diff = (current_date - datetime_obj).days
202208
if days_diff == 0:
203-
return "Today"
209+
return _("Today")
204210
if days_diff >= 7:
205211
return f"{int(days_diff / 7)}w"
206212
return f"{days_diff}d"
@@ -252,6 +258,7 @@ def create_app_notifications_dict(notifications):
252258
name: {
253259
'count': 0,
254260
'title': name.title(),
261+
'translated_title': get_translated_app_title(name),
255262
'notifications': []
256263
}
257264
for name in app_names
@@ -433,16 +440,36 @@ def is_notification_type_channel_editable(app_name, notification_type, channel):
433440
return channel not in COURSE_NOTIFICATION_TYPES[notification_type]['non_editable']
434441

435442

443+
def get_translated_app_title(name):
444+
"""
445+
Returns translated string from notification app_name key
446+
"""
447+
mapping = {
448+
'discussion': _('Discussion'),
449+
'updates': _('Updates'),
450+
'grading': _('Grading'),
451+
}
452+
return mapping.get(name, '')
453+
454+
455+
def get_language_preference_for_users(user_ids):
456+
"""
457+
Returns mapping of user_id and language preference for users
458+
"""
459+
prefs = UserPreference.get_preference_for_users(user_ids, LANGUAGE_KEY)
460+
return {pref.user_id: pref.value for pref in prefs}
461+
462+
436463
def get_text_for_notification_type(notification_type):
437464
"""
438465
Returns text for notification type
439466
"""
440-
app_name = COURSE_NOTIFICATION_APPS.get(notification_type, {}).get('notification_app')
467+
app_name = COURSE_NOTIFICATION_TYPES.get(notification_type, {}).get('notification_app')
441468
if not app_name:
442469
return ""
443470
mapping = {
444-
'discussion': 'post',
445-
'updates': 'update',
446-
'grading': 'assessment',
471+
'discussion': _('post'),
472+
'updates': _('update'),
473+
'grading': _('assessment'),
447474
}
448475
return mapping.get(app_name, "")

openedx/core/djangoapps/notifications/templates/notifications/digest_content.html

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
{% load i18n %}
12
{% for notification_app in email_content %}
23
<h3 style="font-size: 22px; font-weight:700; line-height:28px; margin: 0.75rem 0 0;">
3-
{{ notification_app.title }}
4+
{{ notification_app.translated_title }}
45
</h3>
56
{% if notification_app.help_text %}
67
<p style="margin: 0; height: 1.5rem; font-weight: 400; font-size: 14px; line-height: 24px">
@@ -10,7 +11,7 @@ <h3 style="font-size: 22px; font-weight:700; line-height:28px; margin: 0.75rem 0
1011
{% if notification_app.help_text_url %}
1112
<span style="float:right; margin-right: 0.25rem">
1213
<a href="{{notification_app.help_text_url}}" style="text-decoration: none; color: #00688D">
13-
View all
14+
{% trans "View all" as tmsg %}{{ tmsg | force_escape }}
1415
</a>
1516
</span>
1617
{% endif %}
@@ -46,7 +47,7 @@ <h3 style="font-size: 22px; font-weight:700; line-height:28px; margin: 0.75rem 0
4647
</span>
4748
<span style="float: right">
4849
<a href="{{notification.content_url}}" style="text-decoration: none; color: #00688D">
49-
View
50+
{% trans "View" as tmsg %}{{ tmsg | force_escape }}
5051
</a>
5152
</span>
5253
</blockquote>
@@ -60,7 +61,7 @@ <h3 style="font-size: 22px; font-weight:700; line-height:28px; margin: 0.75rem 0
6061
{% if notification_app.show_remaining_count %}
6162
<p style="margin: 0; height: 0.75rem; font-weight: 400; font-size: 14px; line-height: 24px;">
6263
<a href="{{notification_app.url}}" style="color: #00688d; margin: 0; float:right; text-decoration: none;">
63-
+ {{ notification_app.remaining_count }} more
64+
+ {{ notification_app.remaining_count }} {% trans "more" as tmsg %}{{ tmsg | force_escape }}
6465
</a>
6566
</p>
6667
{% endif %}

openedx/core/djangoapps/notifications/templates/notifications/digest_footer.html

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{% load i18n %}
12
<table cellpadding="0" cellspacing="0" style="color:black; font-weight:400; font-size:12px;line-height:20px" width="100%">
23
<tbody>
34
<tr>
@@ -31,18 +32,22 @@
3132
<tr>
3233
<td>
3334
<p style="margin: 1.5rem 0 0 0;">
34-
{{ footer_email_reason|default:"You are receiving this email because you have subscribed to email digest" }}
35+
{% if footer_email_reason %}
36+
{{ footer_email_reason }}
37+
{% else %}
38+
{% trans "You are receiving this email because you have subscribed to email digest" as tmsg %}{{ tmsg | force_escape }}
39+
{% endif %}
3540
</p>
3641
<p style="margin: 0.625rem 0">
3742
<a href="{{notification_settings_url}}" rel="noopener noreferrer" target="_blank" style="color: black">
38-
Notification Settings
43+
{% trans "Notification Settings" as tmsg %}{{ tmsg | force_escape }}
3944
</a>
4045
<a href="{{unsubscribe_url}}" rel="noopener noreferrer" target="_blank" style="color: black; margin-left: 1rem">
41-
Unsubscribe from email digest for learning activity
46+
{% trans "Unsubscribe from email digest for learning activity" as tmsg %}{{ tmsg | force_escape }}
4247
</a>
4348
</p>
4449
<p>
45-
&copy; {% now "Y" %} {{ platform_name }}. All Rights Reserved <br/>
50+
&copy; {% now "Y" %} {{ platform_name }}. {% trans "All rights reserved" as tmsg %}{{ tmsg | force_escape }} <br/>
4651
{{ mailing_address }}
4752
</p>
4853
</td>

openedx/core/djangoapps/notifications/templates/notifications/digest_header.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{% load i18n %}
12
<table
23
border="0"
34
cellpadding="0"
@@ -8,7 +9,7 @@
89
<tr align="right">
910
<td>
1011
<a href="{{unsubscribe_url}}" rel="noopener noreferrer" target="_blank" style="color: white; text-decoration: none; font-size: 12px; line-height: 10px">
11-
Unsubscribe
12+
{% trans "Unsubscribe" as tmsg %}{{ tmsg | force_escape }}
1213
</a>
1314
</td>
1415
</tr>
@@ -20,7 +21,11 @@
2021
<tr style="height: 20px"></tr>
2122
<tr align="center">
2223
<td style="font-family: Inter, Arial, Verdana, sans-serif; font-size: 32px; font-style: normal; font-weight: 700; line-height: 36px">
23-
{{ digest_frequency }} email digest
24+
{% if digest_frequency == "Weekly" %}
25+
{% trans "Weekly email digest" as tmsg %}{{ tmsg | force_escape }}
26+
{% else %}
27+
{% trans "Daily email digest" as tmsg %}{{ tmsg | force_escape }}
28+
{% endif %}
2429
</td>
2530
</tr>
2631
<tr style="height: 10px"></tr>
@@ -55,7 +60,7 @@
5560
</tr>
5661
<tr align="center">
5762
<td style="font-weight: 600; font-size: 14px; line-height: 20px; padding: 0">
58-
{{update.title}}
63+
{{update.translated_title}}
5964
</td>
6065
</tr>
6166
</tbody>

openedx/core/djangoapps/notifications/templates/notifications/edx_ace/email_digest/email/body.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{% load i18n %}
12
<head>
23
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'" rel="stylesheet" type="text/css">
34
</head>
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
{{ digest_frequency }} Notifications Digest for {% if digest_frequency == "Weekly" %}the Week of {% endif %}{{ start_date }}
1+
{% load i18n %}
2+
3+
{% if digest_frequency == "Weekly" %}
4+
{% trans "Weekly Notifications Digest for the Week of" %} {{ start_date }}
5+
{% else %}
6+
{% trans "Daily Notifications Digest for" %} {{ start_date }}
7+
{% endif %}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
{% load i18n %}
2+
{% get_current_language as LANGUAGE_CODE %}
13
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
2-
<title lang="en">{{ platform_name }}</title>
4+
<title lang="{{ LANGUAGE_CODE|default:'en' }}">{{ platform_name }}</title>
35
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>

0 commit comments

Comments
 (0)