diff --git a/django_email_learning/personalised/api/views.py b/django_email_learning/personalised/api/views.py index ec036738..d757eae7 100644 --- a/django_email_learning/personalised/api/views.py +++ b/django_email_learning/personalised/api/views.py @@ -5,6 +5,7 @@ QuestionResponse, ) from django_email_learning.services import jwt_service +from django.utils.translation import gettext as _ from django_email_learning.models import ( ContentDelivery, QuizSubmission, @@ -79,7 +80,7 @@ def post(self, request, *args, **kwargs): # type: ignore[no-untyped-def] delivery.update_hash() if passed: - message = "Congratulations! You have passed the quiz." + message = _("Congratulations! You have passed the quiz.") delivery = delivery.schedule_next_delivery() if not delivery: enrolment.graduate() @@ -91,14 +92,18 @@ def post(self, request, *args, **kwargs): # type: ignore[no-untyped-def] ).count() if failed_submissions_count > 1: - message = "You have failed the quiz twice. Unfortunatly you can not continue the course on this enrollment. But you can enroll again to retake the course." + message = _( + "You have failed the quiz twice. Unfortunatly you can not continue the course on this enrollment. But you can enroll again to retake the course." + ) logger.info( f"Learner ID {enrolment.learner.id} has failed the quiz twice for Course {enrolment.course.title}. " f"Marking enrollment as failed." ) enrolment.fail() else: - message = "You have failed the quiz. You will receive another chance to retake it tomorrow." + message = _( + "You have failed the quiz. You will receive another chance to retake it tomorrow." + ) logger.info( f"Learner ID {enrolment.learner.id} has failed the quiz for Course {enrolment.course.title}. " f"Scheduling a retry for the next day." diff --git a/django_email_learning/personalised/views.py b/django_email_learning/personalised/views.py index 019cc50d..5b727a9d 100644 --- a/django_email_learning/personalised/views.py +++ b/django_email_learning/personalised/views.py @@ -1,6 +1,7 @@ from django.views import View from django.views.generic.base import TemplateResponseMixin from django.http import HttpResponse +from django.utils.translation import gettext as _ from django.urls import reverse from django_email_learning.models import ContentDelivery, EnrollmentStatus from django_email_learning.services import jwt_service @@ -14,7 +15,11 @@ class ErrrorLoggingMixin(TemplateResponseMixin): def errr_response( - self, message: str, exception: Exception | None, status_code: int = 500 + self, + message: str, + exception: Exception | None, + status_code: int = 500, + title: str = _("Error"), ) -> HttpResponse: error_ref = uuid.uuid4().hex if exception: @@ -26,7 +31,8 @@ def errr_response( f"{message} - Ref: {error_ref}", extra={"error_ref": error_ref} ) return self.render_to_response( - context={"ref": error_ref, "error_message": message}, status=status_code + context={"ref": error_ref, "error_message": message, "page_title": title}, + status=status_code, ) @@ -44,18 +50,22 @@ def get(self, request, *args, **kwargs) -> HttpResponse: # type: ignore[no-unty enrolment = delivery.enrollment if enrolment.status != EnrollmentStatus.ACTIVE: return self.errr_response( - message="Quiz is not valid anymore", + message=_("Quiz is not valid anymore"), exception=ValueError("Enrolment is not active"), + title=_("Invalid Quiz"), ) quiz = delivery.course_content.quiz if not quiz: return self.errr_response( - message="No quiz associated with this link", exception=None + message=_("No quiz associated with this link"), + exception=None, + title=_("Invalid Quiz"), ) if not delivery.course_content.is_published: return self.errr_response( - message="No valid quiz associated with this link", + message=_("No valid quiz associated with this link"), exception=ValueError("Quiz is not published"), + title=_("Invalid Quiz"), ) quiz_data = PublicQuizSerializer.model_validate(quiz).model_dump() if question_ids: @@ -75,19 +85,30 @@ def get(self, request, *args, **kwargs) -> HttpResponse: # type: ignore[no-unty except ContentDelivery.DoesNotExist as e: return self.errr_response( - message="An error occurred while retrieving the quiz", exception=e + message=_("An error occurred while retrieving the quiz"), + exception=e, + title=_("Error"), ) except KeyError as e: return self.errr_response( - message="The link is not valid", exception=e, status_code=400 + message=_("The link is not valid"), + exception=e, + status_code=400, + title=_("Invalid Link"), ) except jwt_service.InvalidTokenException as e: return self.errr_response( - message="The link is not valid", exception=e, status_code=400 + message=_("The link is not valid"), + exception=e, + status_code=400, + title=_("Invalid Link"), ) except jwt_service.ExpiredTokenException as e: return self.errr_response( - message="The link has expired", exception=e, status_code=410 + message=_("The link has expired"), + exception=e, + status_code=410, + title=_("Expired Link"), ) @@ -99,23 +120,26 @@ def get(self, request, *args, **kwargs) -> HttpResponse: # type: ignore[no-unty token = request.GET["token"] except KeyError as e: return self.errr_response( - message="The verification link is not valid.", + message=_("The verification link is not valid."), exception=e, status_code=400, + title=_("Invalid Link"), ) try: decoded = jwt_service.decode_jwt(token=token) except jwt_service.InvalidTokenException as e: return self.errr_response( - message="The verification link is not valid.", + message=_("The verification link is not valid."), exception=e, status_code=400, + title=_("Invalid Link"), ) except jwt_service.ExpiredTokenException as e: return self.errr_response( - message="The verification link has expired.", + message=_("The verification link has expired."), exception=e, status_code=410, + title=_("Expired Link"), ) enrollment_id = decoded["enrollment_id"] @@ -130,7 +154,9 @@ def get(self, request, *args, **kwargs) -> HttpResponse: # type: ignore[no-unty command.execute() except Exception as e: return self.errr_response( - message="An error occurred during enrollment verification.", exception=e + message=_("An error occurred during enrollment verification."), + exception=e, + title=_("Verification Error"), ) - return self.render_to_response(context={"page_title": "Enrollment Verified"}) + return self.render_to_response(context={"page_title": _("Enrollment Verified")}) diff --git a/django_email_learning/platform/views.py b/django_email_learning/platform/views.py index 53d3b1ea..573617e1 100644 --- a/django_email_learning/platform/views.py +++ b/django_email_learning/platform/views.py @@ -2,6 +2,7 @@ from django.views.generic import TemplateView from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator +from django.utils.translation import gettext as _ from django.urls import reverse from django_email_learning.models import Organization, OrganizationUser, Course from django_email_learning.decorators import ( @@ -70,7 +71,7 @@ class Courses(BasePlatformView): def get_context_data(self, **kwargs) -> dict: # type: ignore[no-untyped-def] context = super().get_context_data(**kwargs) - context["page_title"] = "Courses" + context["page_title"] = _("Courses") return context @@ -83,7 +84,7 @@ def get_context_data(self, **kwargs) -> dict: # type: ignore[no-untyped-def] context = super().get_context_data(**kwargs) course = Course.objects.get(pk=self.kwargs["course_id"]) context["course"] = course - context["page_title"] = f"Course: {course.title}" + context["page_title"] = _("Course: %(title)s") % {"title": course.title} return context @@ -94,7 +95,7 @@ class Organizations(BasePlatformView): def get_context_data(self, **kwargs): # type: ignore[no-untyped-def] context = super().get_context_data(**kwargs) - context["page_title"] = "Organizations" + context["page_title"] = _("Organizations") return context @@ -105,5 +106,5 @@ class Learners(BasePlatformView): def get_context_data(self, **kwargs): # type: ignore[no-untyped-def] context = super().get_context_data(**kwargs) - context["page_title"] = "Learners" + context["page_title"] = _("Learners") return context diff --git a/django_email_learning/public/views.py b/django_email_learning/public/views.py index 1ad1943f..868688c6 100644 --- a/django_email_learning/public/views.py +++ b/django_email_learning/public/views.py @@ -3,6 +3,7 @@ from django_email_learning.models import Organization, Course from django.http import Http404 from django.urls import reverse +from django.utils.translation import gettext as _ from django.conf import settings from django_email_learning.public.serializers import ( OrganizationSerializer, @@ -29,7 +30,7 @@ def get_context_data(self, **kwargs) -> dict: # type: ignore[no-untyped-def] if organization_details.exists(): organization = organization_details.first() if not organization: - raise Http404("Organization does not exist") + raise Http404(_("Organization does not exist")) courses = [] for course in organization.courses: course_data = PublicCourseSerializer( @@ -59,4 +60,4 @@ def get_context_data(self, **kwargs) -> dict: # type: ignore[no-untyped-def] return context # If organization not found, raise 404 - raise Http404("Organization does not exist") + raise Http404(_("Organization does not exist")) diff --git a/django_email_learning/services/command_models/enroll_command.py b/django_email_learning/services/command_models/enroll_command.py index de858c04..bb2951c0 100644 --- a/django_email_learning/services/command_models/enroll_command.py +++ b/django_email_learning/services/command_models/enroll_command.py @@ -16,6 +16,7 @@ from django_email_learning.services import jwt_service from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string +from django.utils.translation import gettext as _ from django.conf import settings from django.urls import reverse from typing import Literal @@ -106,7 +107,7 @@ def execute(self) -> None: else None, } email_service = EmailSenderService() - subject = "Verify your enrollment" + subject = _("Verify your enrollment") body = render_to_string( "emails/enrolment_verification.txt", template_context, diff --git a/django_email_learning/services/command_models/verify_enrollment_command.py b/django_email_learning/services/command_models/verify_enrollment_command.py index 5f19d060..45141cbb 100644 --- a/django_email_learning/services/command_models/verify_enrollment_command.py +++ b/django_email_learning/services/command_models/verify_enrollment_command.py @@ -13,6 +13,7 @@ from django_email_learning.services.email_sender_service import EmailSenderService from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string +from django.utils.translation import gettext as _ from typing import Literal @@ -59,7 +60,7 @@ def execute(self) -> None: # Send confirmation email email_service = EmailSenderService() - subject = "Enrollment Verified" + subject = _("Enrollment Verified") body = render_to_string( "emails/enrollment_verified.txt", { diff --git a/django_email_learning/templates/emails/base.html b/django_email_learning/templates/emails/base.html index d978baa0..d9ee5520 100644 --- a/django_email_learning/templates/emails/base.html +++ b/django_email_learning/templates/emails/base.html @@ -1,3 +1,4 @@ +{% load i18n %} {% with brand_color="#7c86ff" %} @@ -24,6 +25,16 @@ background-color: #f9f9f9; } + .locale { + {% get_current_language_bidi as IS_RTL %} + {% if IS_RTL %} + direction: rtl; + text-align: right; + {% else %} + direction: ltr; + text-align: left; + {% endif %} + } @media screen and (max-width: 480px) { .mobile-content { @@ -56,7 +67,7 @@
-
+
{% block content %}{% endblock %}
diff --git a/django_email_learning/templates/emails/enrollment_verified.html b/django_email_learning/templates/emails/enrollment_verified.html index 60116882..5ffcd4fe 100644 --- a/django_email_learning/templates/emails/enrollment_verified.html +++ b/django_email_learning/templates/emails/enrollment_verified.html @@ -1,22 +1,23 @@ {% extends "emails/base.html" %} +{% load i18n %} {% block content %} -

Hello there,

+

{% translate "Hello there," %}

-

Congratulations! Your enrollment for "{{ course_title }}" has been successfully verified and confirmed.

+

{% blocktranslate %}Congratulations! Your enrollment for {{ course_title }} has been successfully verified and confirmed.{% endblocktranslate %}

-

We're excited to have you join our learning community. Here's what you can expect:

+

{% translate "We're excited to have you join our learning community. Here's what you can expect:" %}

-

Your learning adventure begins soon! Keep an eye on your inbox for the first lesson and welcome materials.

+

{% translate "Your learning adventure begins soon! Keep an eye on your inbox for the first lesson and welcome materials." %}

-

Welcome aboard!

+

{% translate "Welcome aboard!" %}

-

Best regards,
-The {{ organization_name }} Team

+

{% translate "Best regards," %}
+{% blocktranslate %}The {{ organization_name }} Team{% endblocktranslate %}

{% endblock %} diff --git a/django_email_learning/templates/emails/enrollment_verified.txt b/django_email_learning/templates/emails/enrollment_verified.txt index 9edcbb51..764cdad0 100644 --- a/django_email_learning/templates/emails/enrollment_verified.txt +++ b/django_email_learning/templates/emails/enrollment_verified.txt @@ -1,16 +1,17 @@ -Hello there, +{% load i18n %} +{% translate "Hello there" %}, -Congratulations! Your enrollment for "{{ course_title }}" has been successfully verified and confirmed. +{% blocktranslate %}Congratulations! Your enrollment for "{{ course_title }}" has been successfully verified and confirmed.{% endblocktranslate %} -We're excited to have you join our learning community. Here's what you can expect: +{% translate "We're excited to have you join our learning community. Here's what you can expect:" %} -• Course materials will be delivered directly to this email address -• You'll receive structured lessons and assignments on a regular schedule -• Interactive content and assessments will help track your progress +• {% translate "Course materials will be delivered directly to this email address" %} +• {% translate "You'll receive structured lessons and assignments on a regular schedule" %} +• {% translate "Interactive content and assessments will help track your progress" %} -Your learning adventure begins soon! Keep an eye on your inbox for the first lesson and welcome materials. +{% translate "Your learning adventure begins soon! Keep an eye on your inbox for the first lesson and welcome materials." %} -Welcome aboard! +{% translate "Welcome aboard!" %} -Best regards, -The {{ organization_name }} Team +{% translate "Best regards," %} +{% blocktranslate %}The {{ organization_name }} Team{% endblocktranslate %} diff --git a/django_email_learning/templates/emails/enrolment_verification.html b/django_email_learning/templates/emails/enrolment_verification.html index 578719a9..651da9ec 100644 --- a/django_email_learning/templates/emails/enrolment_verification.html +++ b/django_email_learning/templates/emails/enrolment_verification.html @@ -1,48 +1,48 @@ {% extends "emails/base.html" %} +{% load i18n %} {% block content %} -

Welcome!

+

{% translate "Welcome!" %}

-

Hello there,

+

{% translate "Hello there," %}

- Congratulations on enrolling in {{ course_title }}! - We're excited to have you join our learning community. + {% blocktranslate %}Congratulations on enrolling in {{ course_title }}!{% endblocktranslate %} + {% translate "We're excited to have you join our learning community." %}

- To get started, please verify your email address by clicking the button below: + {% translate "To get started, please verify your email address by clicking the button below:" %}

- Verify Your Email Address + {% translate "Verify Your Email Address" %}
{% if support_imap_interface %}
-

Alternative Verification Method

+

{% translate "Alternative Verification Method" %}

- You can also verify your email by sending a message to - {{ imap_email_address }} - with your verification code in the subject line or email body. - Simply forward this email to complete the verification process. + {% blocktranslate %}You can also verify your email by sending a message to {{ imap_email_address }}{% endblocktranslate %} + {% translate "with your verification code in the subject line or email body." %} + {% translate "Simply forward this email to complete the verification process." %}

- Verification Code: + {% translate "Verification Code:" %} {{ verification_code }}

{% endif %}
-

If you didn't sign up for this course, you can safely ignore this email.

+

{% translate "If you didn't sign up for this course, you can safely ignore this email." %}

-

Best regards,

+

{% translate "Best regards," %}

{{ organization_name }}

{% endblock %} diff --git a/django_email_learning/templates/emails/enrolment_verification.txt b/django_email_learning/templates/emails/enrolment_verification.txt index 13be1435..0aba3ec8 100644 --- a/django_email_learning/templates/emails/enrolment_verification.txt +++ b/django_email_learning/templates/emails/enrolment_verification.txt @@ -1,16 +1,18 @@ -Hello there, +{% load i18n %} -Congratulations on enrolling in {{ course_title }}! -We're excited to have you join our learning community. +{% translate "Hello there," %} -To get started, please verify your email address by visiting the link below: +{% blocktranslate %}Congratulations on enrolling in {{ course_title }}!{% endblocktranslate %} +{% translate "We're excited to have you join our learning community." %} + +{% translate "To get started, please verify your email address by visiting the link below:" %} {{ verification_link }} {% if support_imap_interface %} -Alternatively, you can also verify your email by sending the verification code {{ verification_code }} via an email to {{ imap_email_address }}. +{% blocktranslate %}Alternatively, you can also verify your email by sending the verification code {{ verification_code }} via an email to {{ imap_email_address }}.{% endblocktranslate %} {% endif %} -If you didn't sign up for this course, you can safely ignore this email. +{% translate "If you didn't sign up for this course, you can safely ignore this email." %} -Best Regards, +{% translate "Best Regards," %} {{ organization_name }} diff --git a/django_email_learning/templates/emails/quiz.html b/django_email_learning/templates/emails/quiz.html index 2644402d..c3cb4d89 100644 --- a/django_email_learning/templates/emails/quiz.html +++ b/django_email_learning/templates/emails/quiz.html @@ -1,21 +1,20 @@ {% extends "emails/base.html" %} - +{% load i18n %} {% block content %}

{{ quiz.title }}

-

Dear Learner,

- -

Your quiz is now available for completion. You have {{ quiz.deadline_days }} days from today to submit your responses.

+

{% translate "Dear Learner," %}

-

To successfully pass this quiz, you must achieve a minimum score of {{ quiz.required_score }}.

+

{% blocktranslate with deadline_days=quiz.deadline_days %}Your quiz is now available for completion. You have {{ deadline_days }} days from today to submit your responses.{% endblocktranslate %}

-

Please click the link below to access and submit your quiz:

+

{% blocktranslate with score=quiz.required_score %}To successfully pass this quiz, you must achieve a minimum score of {{ score }}.{% endblocktranslate %}

+

{% translate "Please click the link below to access and submit your quiz:" %}

- Start Quiz + {% translate "Start Quiz" %}
-

Best of luck!

+

{% translate "Best of luck!" %}

{% endblock %} diff --git a/django_email_learning/templates/emails/quiz.txt b/django_email_learning/templates/emails/quiz.txt index 07620bdd..932df18d 100644 --- a/django_email_learning/templates/emails/quiz.txt +++ b/django_email_learning/templates/emails/quiz.txt @@ -1,10 +1,11 @@ +{% load i18n %} {{ quiz.title }} -Dear Learner, +{% translate "Dear Learner," %} -Your quiz is now available for completion. You have {{ quiz.deadline_days }} days from today to submit your responses. +{% blocktranslate with with deadline_days=quiz.deadline_days %}Your quiz is now available for completion. You have {{ deadline_days }} days from today to submit your responses.{% endblocktranslate %} -To successfully pass this quiz, you must achieve a minimum score of {{ quiz.required_score }}. +{% blocktranslate with score=quiz.required_score %}To successfully pass this quiz, you must achieve a minimum score of {{ score }}.{% endblocktranslate %} -Please click the link below to access and submit your quiz: +{% translate "Please click the link below to access and submit your quiz:" %} {{ link }} diff --git a/django_email_learning/templates/personalised/base.html b/django_email_learning/templates/personalised/base.html index c8417840..164de682 100644 --- a/django_email_learning/templates/personalised/base.html +++ b/django_email_learning/templates/personalised/base.html @@ -1,5 +1,6 @@ {% load django_vite %} {% load static %} +{% load i18n %} @@ -15,7 +16,8 @@ {% block title %}{{ page_title }}{% endblock %} - + {% get_current_language_bidi as IS_RTL %} +
diff --git a/django_email_learning/templates/personalised/quiz_public.html b/django_email_learning/templates/personalised/quiz_public.html index 763fdebd..95e0ba4f 100644 --- a/django_email_learning/templates/personalised/quiz_public.html +++ b/django_email_learning/templates/personalised/quiz_public.html @@ -1,4 +1,5 @@ {% extends "personalised/base.html" %} +{% load i18n %} {% load django_vite %} {% block head_script %} {% vite_asset 'personalised/quiz_public/Quiz.jsx' %} {% endblock %} diff --git a/django_email_learning/templates/personalised/verify_enrollment.html b/django_email_learning/templates/personalised/verify_enrollment.html index 12d09c19..fdc1133d 100644 --- a/django_email_learning/templates/personalised/verify_enrollment.html +++ b/django_email_learning/templates/personalised/verify_enrollment.html @@ -1,5 +1,11 @@ {% extends "personalised/base.html" %} +{% load i18n %} {% load django_vite %} {% block head_script %} + {% vite_asset 'personalised/verify_enrollment/Verification.jsx' %} {% endblock %} diff --git a/django_email_learning/templates/platform/base.html b/django_email_learning/templates/platform/base.html index 4283a822..813d2cd3 100644 --- a/django_email_learning/templates/platform/base.html +++ b/django_email_learning/templates/platform/base.html @@ -1,23 +1,32 @@ {% load django_vite %} {% load static %} +{% load i18n %} +{% get_current_language_bidi as IS_RTL %} - + {% vite_hmr_client %} {% vite_react_refresh %} - {% block extra_head %}{% endblock %} + {% block extra_head %}{% endblock %} {% block title %}{{ page_title }}{% endblock %} +
diff --git a/django_email_learning/templates/platform/course.html b/django_email_learning/templates/platform/course.html index 29d57be3..e065b0e5 100644 --- a/django_email_learning/templates/platform/course.html +++ b/django_email_learning/templates/platform/course.html @@ -1,9 +1,65 @@ {% extends "platform/base.html" %} {% load django_vite %} {% block extra_head %} +{% load i18n %} {% vite_asset 'platform/course/Course.jsx' %} {% endblock %} diff --git a/django_email_learning/templates/platform/courses.html b/django_email_learning/templates/platform/courses.html index 34be41b6..72d354de 100644 --- a/django_email_learning/templates/platform/courses.html +++ b/django_email_learning/templates/platform/courses.html @@ -1,5 +1,52 @@ {% extends "platform/base.html" %} {% load django_vite %} +{% load i18n %} {% block extra_head %} + {% vite_asset 'platform/courses/Courses.jsx' %} {% endblock %} diff --git a/django_email_learning/templates/platform/learners.html b/django_email_learning/templates/platform/learners.html index 7de9da61..b541cd7a 100644 --- a/django_email_learning/templates/platform/learners.html +++ b/django_email_learning/templates/platform/learners.html @@ -1,5 +1,37 @@ {% extends "platform/base.html" %} {% load django_vite %} +{% load i18n %} {% block extra_head %} + {% vite_asset 'platform/learners/Learners.jsx' %} {% endblock %} diff --git a/django_email_learning/templates/platform/organizations.html b/django_email_learning/templates/platform/organizations.html index 94f7f830..18615130 100644 --- a/django_email_learning/templates/platform/organizations.html +++ b/django_email_learning/templates/platform/organizations.html @@ -1,5 +1,25 @@ {% extends "platform/base.html" %} {% load django_vite %} +{% load i18n %} {% block extra_head %} + {% vite_asset 'platform/organizations/Organizations.jsx' %} {% endblock %} diff --git a/django_email_learning/templates/public/base.html b/django_email_learning/templates/public/base.html index c8417840..f1893181 100644 --- a/django_email_learning/templates/public/base.html +++ b/django_email_learning/templates/public/base.html @@ -1,11 +1,14 @@ {% load django_vite %} +{% load i18n %} {% load static %} +{% get_current_language_bidi as IS_RTL %} - + {% vite_hmr_client %} {% vite_react_refresh %} @@ -15,7 +18,8 @@ {% block title %}{{ page_title }}{% endblock %} - + {% get_current_language_bidi as IS_RTL %} +
diff --git a/django_email_learning/templates/public/organization.html b/django_email_learning/templates/public/organization.html index 692bb9b1..a85efd1c 100644 --- a/django_email_learning/templates/public/organization.html +++ b/django_email_learning/templates/public/organization.html @@ -1,9 +1,23 @@ {% extends "public/base.html" %} {% load django_vite %} +{% load i18n %} {% block head_script %} {% vite_asset 'public/organization/Organization.jsx' %} {% endblock %} diff --git a/frontend/personalised/quiz_public/Quiz.jsx b/frontend/personalised/quiz_public/Quiz.jsx index cf21d053..b81b1307 100644 --- a/frontend/personalised/quiz_public/Quiz.jsx +++ b/frontend/personalised/quiz_public/Quiz.jsx @@ -23,7 +23,7 @@ const Quiz = () => { answerCounter += selectedAnswers[i].answers.length; } if (answerCounter === 0) { - setWarning("You have not selected any answers. Are you sure you want to submit an empty quiz?"); + setWarning(localeMessages['no_answer_warning']); } else { setWarning(""); } @@ -61,8 +61,7 @@ const Quiz = () => { {showQuestions ? - Please select all correct answers for each question. Note that some questions may have multiple correct answers. - This quiz uses negative marking for incorrect choices; if you are unsure, it is better to leave the question unanswered. + { localeMessages['quiz_intro'] } @@ -85,21 +84,21 @@ const Quiz = () => { ))} - + : - {isPassed !== null && (isPassed ? {message} Your score is {score}%. : - {message} Your score is {score}%.)} + {isPassed !== null && (isPassed ? {message} {localeMessages['your_score']}: {score}%. : + {message} {localeMessages['your_score']}: {score}%.)} } - : Error loading quiz: {error_message} {ref && `(Ref: ${ref})`}} + : {localeMessages['error_loading_quiz']}: {error_message} {ref && `(Ref: ${ref})`}} setDialogOpen(false)} fullWidth maxWidth="sm"> - Ready to submit? + {localeMessages['ready_to_submit']} {warning ? {warning} : - Please keep in mind that this quiz uses negative marking for incorrect answers. If you are unsure of an answer, it may be better to leave it blank.} + {localeMessages['submit_quiz_note']}} - - + + diff --git a/frontend/personalised/verify_enrollment/Verification.jsx b/frontend/personalised/verify_enrollment/Verification.jsx index 3254500b..913cd3e8 100644 --- a/frontend/personalised/verify_enrollment/Verification.jsx +++ b/frontend/personalised/verify_enrollment/Verification.jsx @@ -6,7 +6,7 @@ import Layout from '../../public/components/Layout.jsx'; const Verification = () => { return { !error_message ? - You have successfully verified your enrollment. + { localeMessages['verify_enrollment_success'] } : {error_message} (ref: {ref}) } diff --git a/frontend/platform/course/Course.jsx b/frontend/platform/course/Course.jsx index 67be86ae..90293fa6 100644 --- a/frontend/platform/course/Course.jsx +++ b/frontend/platform/course/Course.jsx @@ -79,7 +79,7 @@ function Course() { console.log("Opening lesson editor for content:", content); setDialogOpen(true); setDialogContent( - {userRole !== 'viewer' && <> - + } + setDialogOpen(true);}}>{localeMessages["add_quiz"]} } tableEventHandler(event)} /> diff --git a/frontend/platform/course/components/ContentTable.jsx b/frontend/platform/course/components/ContentTable.jsx index d3054dfa..fd383f60 100644 --- a/frontend/platform/course/components/ContentTable.jsx +++ b/frontend/platform/course/components/ContentTable.jsx @@ -116,11 +116,11 @@ const ContentTable = ({ courseId, eventHandler, loaded = false }) => { { userRole !== 'viewer' && } - Title - Waiting time - type - Published - {userRole !== 'viewer' && Actions} + {localeMessages["title"]} + {localeMessages["waiting_time"]} + {localeMessages["type"]} + {localeMessages["published"]} + {userRole !== 'viewer' && {localeMessages["actions"]}} @@ -140,17 +140,17 @@ const ContentTable = ({ courseId, eventHandler, loaded = false }) => { eventHandler(event); } }}> - { userRole !== 'viewer' && startDrag(content.id)} />} - {let event = {type: 'content_clicked', content_id: content.id}; eventHandler(event);}} color='primary.dark' sx={{ cursor: 'pointer'}}>{content.title} - {formatPeriod(content.waiting_period)} - {content.type.charAt(0).toUpperCase() + content.type.slice(1)} - TogglePublishContent(content.id, !content.is_published)} disabled={userRole == 'viewer'} /> - {userRole !== 'viewer' && - deleteContent(content.id)}> + {formatPeriod(content.waiting_period)} + {localeMessages[content.type]} + TogglePublishContent(content.id, !content.is_published)} disabled={userRole == 'viewer'} /> + {userRole !== 'viewer' && + deleteContent(content.id)}> } diff --git a/frontend/platform/course/components/LessonForm.jsx b/frontend/platform/course/components/LessonForm.jsx index 01371c70..792b21c5 100644 --- a/frontend/platform/course/components/LessonForm.jsx +++ b/frontend/platform/course/components/LessonForm.jsx @@ -19,7 +19,7 @@ function LessonForm({ header, initialTitle, initialContent, onContentChange, can const addLesson = () =>{ if (!validateForm()) { - setErrorMessage("Please fix the errors in the form before submitting."); + setErrorMessage(localeMessages["fix_errors"]); return; } @@ -54,7 +54,7 @@ function LessonForm({ header, initialTitle, initialContent, onContentChange, can const updateLesson = () => { if (!validateForm()) { - setErrorMessage("Please fix the errors in the form before submitting."); + setErrorMessage(localeMessages["fix_errors"]); return; } @@ -105,13 +105,13 @@ function LessonForm({ header, initialTitle, initialContent, onContentChange, can const validateForm = () => { let isValid = true; if (!title) { - setTitleHelperText("Lesson title is required."); + setTitleHelperText(localeMessages["lesson_title_required"]); isValid = false; } else { setTitleHelperText(""); } if (!content) { - setContentHelperText("Lesson content is required."); + setContentHelperText(localeMessages["lesson_content_required"]); isValid = false; } else { setContentHelperText(""); @@ -127,7 +127,7 @@ function LessonForm({ header, initialTitle, initialContent, onContentChange, can {errorMessage} )} - setTitle(e.target.value)} helperText={titleHelperText} disabled={userRole === 'viewer'} /> + setTitle(e.target.value)} helperText={titleHelperText} disabled={userRole === 'viewer'} /> @@ -136,9 +136,9 @@ function LessonForm({ header, initialTitle, initialContent, onContentChange, can + title={localeMessages["lesson_waiting_tooltip"]}> - setWaitingPeriodUnit(e.target.value)} name="waiting_period_unit" sx={{ width: '150px', mr: 2 }} disabled={userRole === 'viewer'}> + {localeMessages["days"]} + {localeMessages["hours"]} - {userRole !== 'viewer' && } diff --git a/frontend/platform/course/components/QuestionForm.jsx b/frontend/platform/course/components/QuestionForm.jsx index 59359ca0..e301f01e 100644 --- a/frontend/platform/course/components/QuestionForm.jsx +++ b/frontend/platform/course/components/QuestionForm.jsx @@ -6,7 +6,6 @@ import ClearIcon from '@mui/icons-material/Clear'; import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; - const QuestionForm = ({question, index, eventHandler}) => { const [questionText, setQuestionText] = useState(question.text); const [options, setOptions] = useState(question.options || []); @@ -80,10 +79,10 @@ const QuestionForm = ({question, index, eventHandler}) => { {userRole !== 'viewer' && <> - } {addingOption && (<> @@ -112,10 +111,10 @@ const QuestionForm = ({question, index, eventHandler}) => { } }}> - Add + { localeMessages["add"] } @@ -125,9 +124,9 @@ const QuestionForm = ({question, index, eventHandler}) => { - Options - Correct Answer - {userRole !== 'viewer' && Actions} + {localeMessages["options"]} + {localeMessages["correct_answer"]} + {userRole !== 'viewer' && {localeMessages["actions"]}} diff --git a/frontend/platform/course/components/QuizForm.jsx b/frontend/platform/course/components/QuizForm.jsx index aa59335d..e6fa74ac 100644 --- a/frontend/platform/course/components/QuizForm.jsx +++ b/frontend/platform/course/components/QuizForm.jsx @@ -68,11 +68,11 @@ const QuizForm = ({cancelCallback, successCallback, courseId, quizId, contentId, const validateQuiz = () => { if (title.trim() === "") { - setErrorMessage("Quiz title cannot be empty."); + setErrorMessage(localeMessages["quiz_title_empty"]); return false; } if (questions.length === 0) { - setErrorMessage("Quiz must contain at least one question."); + setErrorMessage(localeMessages["at_least_one_question"]); return false; } for (let i = 0; i < questions.length; i++) { @@ -83,12 +83,12 @@ const QuizForm = ({cancelCallback, successCallback, courseId, quizId, contentId, } const options = question.options || []; if (options.length < 2) { - setErrorMessage(`Question ${i + 1} must have at least two answer options.`); + setErrorMessage(localeMessages["at_least_two_options"].replace("QUESTION_NUMBER", i + 1)); return false; } const hasCorrectOption = options.some(option => option.isCorrect); if (!hasCorrectOption) { - setErrorMessage(`Question ${i + 1} must have at least one correct answer.`); + setErrorMessage(localeMessages["at_least_one_correct"].replace("QUESTION_NUMBER", i + 1)); return false; } } @@ -205,11 +205,11 @@ const QuizForm = ({cancelCallback, successCallback, courseId, quizId, contentId, } }} tabIndex={0} focusable="true"> - { quizId ? "Update Quiz" : "New Quiz" } + { quizId ? localeMessages["update_quiz"] : localeMessages["new_quiz"] } {errorMessage && {errorMessage}} - setTitle(e.target.value)} sx={{ mb: 2, width: '100%' }} disabled={userRole === 'viewer'} /> + setTitle(e.target.value)} sx={{ mb: 2, width: '100%' }} disabled={userRole === 'viewer'} /> {userRole !== 'viewer' && } + {localeMessages["add_question"]}} { showQuestionField && ( @@ -224,10 +224,10 @@ const QuizForm = ({cancelCallback, successCallback, courseId, quizId, contentId, @@ -241,17 +241,17 @@ const QuizForm = ({cancelCallback, successCallback, courseId, quizId, contentId, {/* Quiz Settings Section */} - Quiz Settings + {localeMessages["quiz_settings"]} {/* Row 1: Required Score and Waiting Period */} - Required Score to Pass + {localeMessages["required_score"]} - Waiting Period + {localeMessages["waiting_period"]} - Deadline to Complete Quiz + {localeMessages["quiz_deadline"]} - Selection Strategy + {localeMessages["question_selection_strategy"]} @@ -350,10 +348,10 @@ const QuizForm = ({cancelCallback, successCallback, courseId, quizId, contentId, - {userRole !== 'viewer' && } diff --git a/frontend/platform/courses/Courses.jsx b/frontend/platform/courses/Courses.jsx index b99593ff..6da490f0 100644 --- a/frontend/platform/courses/Courses.jsx +++ b/frontend/platform/courses/Courses.jsx @@ -98,7 +98,7 @@ function Courses() { return ( , children: setQueryParameters(params)} /> @@ -107,7 +107,7 @@ function Courses() { > - {userRole !== 'viewer' && } + setDialogOpen(true);}}>{localeMessages["add_course"]}} -
+
- Title - Slug - Enabled - {userRole !== 'viewer' && Actions} + {localeMessages["title"]} + {localeMessages["slug"]} + {localeMessages["enabled"]} + {userRole !== 'viewer' && {localeMessages["actions"]}} @@ -132,11 +132,11 @@ function Courses() { key={course.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }} > - + {course.title} - {course.slug} - + {course.slug} + - {userRole !== 'viewer' && + {userRole !== 'viewer' && { showEditCourseDialog(course);}}> { diff --git a/frontend/platform/courses/components/AddImapConnectionForm.jsx b/frontend/platform/courses/components/AddImapConnectionForm.jsx index 6396e317..bf0d8c9a 100644 --- a/frontend/platform/courses/components/AddImapConnectionForm.jsx +++ b/frontend/platform/courses/components/AddImapConnectionForm.jsx @@ -54,11 +54,11 @@ function AddImapConnectionForm({onChangeCallback, activeOrganizationId, initialI
{ hasImapConnection && ( - IMAP Connection + {localeMessages["imap_connection"]}
- Course - Status + {localeMessages["course"]} + {localeMessages["status"]} @@ -38,8 +37,8 @@ function EnrolmentList({enrollments, selectHandler}) { ({':hover': {backgroundColor: theme.palette.background.dark, cursor: 'pointer', borderBottomColor: 'secondary.light', borderBottomWidth: 2, borderBottomStyle: 'solid'}})} onClick={() => selectHandler(enrollment.id)}> - {enrollment.course_title} - {enrollment.status} + {enrollment.course_title} + {localeMessages[enrollment.status]} ))} @@ -62,13 +61,13 @@ function Learners(initialQs="") { const [currentPage, setCurrentPage] = useState(1); const eventMap = { - 'registered': {icon: , color: "#00bcd4", title: 'Learner Registered'}, - 'verified': {icon: , color: "#66bb6a", title: 'Learner Verified Email'}, - 'content_sent_lesson': {icon: , color: "#00acc1", title: 'Lesson Content Sent'}, - 'content_sent_quiz': {icon: , color: "#26a69a", title: 'Quiz Sent'}, - 'quiz_submitted': {icon: , color: "#26a69a", title: 'Quiz Submitted'}, - 'course_completed': {icon: , color: "#0097a7", title: 'Course Completed'}, - 'deactivated': {icon: , color: "#b71c1c", title: 'Learner Deactivated'}, + 'registered': {icon: , color: "#00bcd4", title: localeMessages["learner_registered"]}, + 'verified': {icon: , color: "#66bb6a", title: localeMessages["learner_verified"]}, + 'content_sent_lesson': {icon: , color: "#00acc1", title: localeMessages["lesson_sent"]}, + 'content_sent_quiz': {icon: , color: "#26a69a", title: localeMessages["quiz_sent"]}, + 'quiz_submitted': {icon: , color: "#26a69a", title: localeMessages["quiz_submitted"]}, + 'course_completed': {icon: , color: "#0097a7", title: localeMessages["course_completed"]}, + 'deactivated': {icon: , color: "#b71c1c", title: localeMessages["learner_deactivated"]}, }; @@ -86,10 +85,10 @@ function Learners(initialQs="") { .then(data => { setDialogContent( - Enrollment Details + {localeMessages["enrollment_details"]} {data.learner.email} {data.course.title} - Enrollment id: {data.id} + {localeMessages["enrollment_id"]}: {data.id} {data.events.map((event, index) => ( @@ -108,19 +107,19 @@ function Learners(initialQs="") { - + { event.type === "content_sent" ? eventMap["content_sent_"+event.event_data.course_content_type].title : eventMap[event.type].title } { event.type === "quiz_submitted" && <> - Score: {event.event_data.score} - Result: {event.event_data.is_passed ? "Passed" : "Failed"} + {localeMessages["score"]}: {event.event_data.score} + {localeMessages["result"]}: {event.event_data.is_passed ? localeMessages["passed"] : localeMessages["failed"]} } { event.type === "content_sent" && <> {event.event_data.course_content_title} } { event.type === "deactivated" && <> - Reason: {event.event_data.reason} + {localeMessages["reason"]}: {event.event_data.reason} } @@ -203,7 +202,7 @@ function Learners(initialQs="") { return ( @@ -213,8 +212,8 @@ function Learners(initialQs="") { sx={{ mb: '10px', p: '2px 4px', display: 'flex', alignItems: 'center', width: 400 }} > { if (e.key === 'Enter') { search(); } }} /> @@ -227,7 +226,7 @@ function Learners(initialQs="") {
- Learners List + {localeMessages["learners_list"]} diff --git a/frontend/platform/organizations/Organizations.jsx b/frontend/platform/organizations/Organizations.jsx index 831fab08..9fc1ddbf 100644 --- a/frontend/platform/organizations/Organizations.jsx +++ b/frontend/platform/organizations/Organizations.jsx @@ -69,17 +69,19 @@ function Organizations() { }); } + const htmlTag = document.getElementsByTagName("html")[0]; + const deleteConfirmationDialog = (organization) => { setDialogContent( - Confirm Deletion - Are you sure you want to delete the organization "{organization.name}"? All the courses contents and users under this organization will also be deleted. + {localeMessages["confirm_deletion"]} + {localeMessages["are_you_sure_delete_org"].replace("ORGANIZATION_NAME", organization.name)} - + + }}>{localeMessages["delete"]} ); @@ -87,10 +89,10 @@ function Organizations() { } return ( - + - + }}>{localeMessages["add_organization"]} { organizations.length > 0 && (
- Name - Actions + {localeMessages["name"]} + {localeMessages["actions"]} { organizations.map((org) => ( - {org.name} + {org.name} goToUrl(org.public_url)}> { diff --git a/frontend/platform/organizations/components/OrganizationForm.jsx b/frontend/platform/organizations/components/OrganizationForm.jsx index 6f1fb65c..f41da260 100644 --- a/frontend/platform/organizations/components/OrganizationForm.jsx +++ b/frontend/platform/organizations/components/OrganizationForm.jsx @@ -137,28 +137,30 @@ function OrganizationForm({ successCallback, failureCallback, cancelCallback, cr return ( - setName(e.target.value)} /> - setDescription(e.target.value)} /> + setName(e.target.value)} /> + setDescription(e.target.value)} /> { !logoUrl ? - : (<>Organization Logo
- + : (<>{localeMessages["organization_logo"]}
+ )} - +
diff --git a/frontend/public/components/EnrollmentForm.jsx b/frontend/public/components/EnrollmentForm.jsx index 84f2c8d2..3d13a08b 100644 --- a/frontend/public/components/EnrollmentForm.jsx +++ b/frontend/public/components/EnrollmentForm.jsx @@ -13,13 +13,13 @@ const EnrollmentForm = ({course_title, course_slug, organization_id, endpoint, o const validateForm = () => { const email = emailRef.current.value; if (!email) { - setErrorMessage('Email is required'); + setErrorMessage(localeMessages["email_required"]); return false; } // Simple email regex for validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { - setErrorMessage('Please enter a valid email address'); + setErrorMessage(localeMessages["email_invalid"]); return false; } setErrorMessage(''); @@ -54,27 +54,27 @@ const EnrollmentForm = ({course_title, course_slug, organization_id, endpoint, o }) .catch(error => { setIsProcessing(false); - setErrorMessage('Enrollment failed. Please try again.'); + setErrorMessage(localeMessages['enrollment_failed']); }); } } return ( - Enroll for {course_title} + {localeMessages['enrol_for_course'].replace('COURSE_NAME', course_title)} { isProcessing ? : <> {errorMessage && {errorMessage}} - { + { if (e.key === 'Enter') { enroll(); } }} /> - } diff --git a/frontend/public/organization/Organization.jsx b/frontend/public/organization/Organization.jsx index 8fb38336..e6af29e6 100644 --- a/frontend/public/organization/Organization.jsx +++ b/frontend/public/organization/Organization.jsx @@ -45,7 +45,7 @@ function Organization() { { organization.name } - Courses: + {localeMessages['courses']}: { organization.courses.length > 0 ? ( { courses.map((course) => ( @@ -53,16 +53,16 @@ function Organization() { {course.title} - { course.enrolled && You are enrolled in this course. } + { course.enrolled && {localeMessages['enrollment_success']} } ))} ) : ( - No courses available. + {localeMessages['no_courses_available']} )} diff --git a/frontend/src/components/Base.jsx b/frontend/src/components/Base.jsx index 2e13f76a..31c37118 100644 --- a/frontend/src/components/Base.jsx +++ b/frontend/src/components/Base.jsx @@ -55,13 +55,12 @@ function Base({breadCrumbList, children, bottomDrawerParams, organizationIdRefre }, [activeOrganizationId]); - return ( <> ({ body: { margin: 0, padding: 0, backgroundColor: theme.palette.background.dark, color: theme.palette.text.primary } })} /> + sx={{ flexGrow: 1, padding: {sm: 3, xs: 1, md: 5}, width: { md: `calc(100% - ${drawerWidth + 100}px)` }, float: { md: direction === 'rtl' ? 'left' : 'right' } }}> @@ -91,7 +90,8 @@ function Base({breadCrumbList, children, bottomDrawerParams, organizationIdRefre sx={{ position: 'fixed', bottom: 0, - right: 16, + right: direction === 'rtl' ? 'auto' : 16, + left: direction === 'rtl' ? 16 : 'auto', padding: 1, zIndex: 1000, backgroundColor: 'transparent' diff --git a/frontend/src/components/MenuBar.jsx b/frontend/src/components/MenuBar.jsx index 2895348c..8c8525b9 100644 --- a/frontend/src/components/MenuBar.jsx +++ b/frontend/src/components/MenuBar.jsx @@ -83,12 +83,12 @@ function MenuBar({activeOrganizationId, changeOrganizationCallback, showOrganiza if (localStorage.getItem('isPlatformAdmin') == 'true') { pages.push( - { name: 'Organizations', icon: , href: platformBaseUrl + '/organizations/'}, + { name: localeMessages["organizations"], icon: , href: platformBaseUrl + '/organizations/'}, ); } - pages.push({ name: 'Course Management', icon: , href: platformBaseUrl + '/courses/' }); - pages.push({ name: 'Learners', icon: , href: platformBaseUrl + '/learners/' }); + pages.push({ name: localeMessages["course_management"], icon: , href: platformBaseUrl + '/courses/' }); + pages.push({ name: localeMessages["learners"], icon: , href: platformBaseUrl + '/learners/' }); // pages.push({ name: 'Analytics', icon: , href: platformBaseUrl + '/analytics/' }); @@ -102,7 +102,7 @@ function MenuBar({activeOrganizationId, changeOrganizationCallback, showOrganiza Logo - + @@ -111,7 +111,7 @@ function MenuBar({activeOrganizationId, changeOrganizationCallback, showOrganiza - Logo diff --git a/frontend/src/components/RequiredTextField.jsx b/frontend/src/components/RequiredTextField.jsx index 86f724a7..137ef56b 100644 --- a/frontend/src/components/RequiredTextField.jsx +++ b/frontend/src/components/RequiredTextField.jsx @@ -1,8 +1,9 @@ import { TextField } from '@mui/material'; import React from 'react'; -const RequiredTextField = React.forwardRef(({ label, value, onChange, error, helperText, ...props }, ref) => { +const RequiredTextField = React.forwardRef(({ label, value, onChange, error, helperText, sx: passedSx = {}, ...props }, ref) => { console.log('RequiredTextField rendered, ref:', ref); + return ( ); });