Skip to content

Commit

Permalink
feat: Internationalize sent emails (mealie-recipes#3818)
Browse files Browse the repository at this point in the history
  • Loading branch information
p0lycarpio authored Jul 20, 2024
1 parent c205dff commit 60c33b4
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 31 deletions.
25 changes: 24 additions & 1 deletion mealie/lang/messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"recipe-defaults": {
"ingredient-note": "1 Cup Flour",
"step-text": "Recipe steps as well as other fields in the recipe page support markdown syntax.\n\n**Add a link**\n\n[My Link](https://demo.mealie.io)\n"
}
}
},
"mealplan": {
"no-recipes-match-your-rules": "No recipes match your rules"
Expand Down Expand Up @@ -44,5 +44,28 @@
"second": "second|seconds",
"millisecond": "millisecond|milliseconds",
"microsecond": "microsecond|microseconds"
},
"emails": {
"password": {
"subject": "Mealie Forgot Password",
"header_text": "Forgot Password",
"message_top": "You have requested to reset your password.",
"message_bottom": "Please click the button above to reset your password.",
"button_text": "Reset Password"
},
"invitation": {
"subject": "Invitation to join Mealie",
"header_text": "You're Invited!",
"message_top": "You have been invited to join Mealie.",
"message_bottom": "Please click the button above to accept the invitation.",
"button_text": "Accept Invitation"
},
"test": {
"subject": "Mealie Test Email",
"header_text": "Test Email",
"message_top": "This is a test email.",
"message_bottom": "Please click the button above to test the email.",
"button_text": "Open Mealie"
}
}
}
12 changes: 9 additions & 3 deletions mealie/routes/admin/admin_email.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from fastapi import APIRouter
from typing import Annotated

from fastapi import APIRouter, Header

from mealie.routes._base import BaseAdminController, controller
from mealie.schema.admin.email import EmailReady, EmailSuccess, EmailTest
Expand All @@ -15,8 +17,12 @@ async def check_email_config(self):
return EmailReady(ready=self.settings.SMTP_ENABLE)

@router.post("", response_model=EmailSuccess)
async def send_test_email(self, data: EmailTest):
service = EmailService()
async def send_test_email(
self,
data: EmailTest,
accept_language: Annotated[str | None, Header()] = None,
):
service = EmailService(locale=accept_language)
status = False
error = None

Expand Down
17 changes: 13 additions & 4 deletions mealie/routes/groups/controller_invitations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from fastapi import APIRouter, HTTPException, status
from typing import Annotated

from fastapi import APIRouter, Header, HTTPException, status

from mealie.core.security import url_safe_token
from mealie.routes._base import BaseUserController, controller
Expand All @@ -23,14 +25,21 @@ def get_invite_tokens(self):
@router.post("", response_model=ReadInviteToken, status_code=status.HTTP_201_CREATED)
def create_invite_token(self, uses: CreateInviteToken):
if not self.user.can_invite:
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="User is not allowed to create invite tokens")
raise HTTPException(
status.HTTP_403_FORBIDDEN,
detail="User is not allowed to create invite tokens",
)

token = SaveInviteToken(uses_left=uses.uses, group_id=self.group_id, token=url_safe_token())
return self.repos.group_invite_tokens.create(token)

@router.post("/email", response_model=EmailInitationResponse)
def email_invitation(self, invite: EmailInvitation):
email_service = EmailService()
def email_invitation(
self,
invite: EmailInvitation,
accept_language: Annotated[str | None, Header()] = None,
):
email_service = EmailService(locale=accept_language)
url = f"{self.settings.BASE_URL}/register?token={invite.token}"

success = False
Expand Down
12 changes: 9 additions & 3 deletions mealie/routes/users/forgot_password.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from fastapi import APIRouter, Depends
from typing import Annotated

from fastapi import APIRouter, Depends, Header
from sqlalchemy.orm.session import Session

from mealie.db.db_setup import generate_session
Expand All @@ -9,10 +11,14 @@


@router.post("/forgot-password")
def forgot_password(email: ForgotPassword, session: Session = Depends(generate_session)):
def forgot_password(
email: ForgotPassword,
session: Session = Depends(generate_session),
accept_language: Annotated[str | None, Header()] = None,
):
"""Sends an email with a reset link to the user"""
f_service = PasswordResetService(session)
return f_service.send_reset_email(email.email)
return f_service.send_reset_email(email.email, accept_language)


@router.post("/reset-password")
Expand Down
37 changes: 20 additions & 17 deletions mealie/services/email/email_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from pydantic import BaseModel

from mealie.core.root_logger import get_logger
from mealie.lang import local_provider
from mealie.lang.providers import Translator
from mealie.services._base_service import BaseService

from .email_senders import ABCEmailSender, DefaultEmailSender
Expand All @@ -28,10 +30,11 @@ def render_html(self, template: Path) -> str:


class EmailService(BaseService):
def __init__(self, sender: ABCEmailSender | None = None) -> None:
def __init__(self, sender: ABCEmailSender | None = None, locale: str | None = None) -> None:
self.templates_dir = CWD / "templates"
self.default_template = self.templates_dir / "default.html"
self.sender: ABCEmailSender = sender or DefaultEmailSender()
self.translator: Translator = local_provider(locale)

super().__init__()

Expand All @@ -43,33 +46,33 @@ def send_email(self, email_to: str, data: EmailTemplate) -> bool:

def send_forgot_password(self, address: str, reset_password_url: str) -> bool:
forgot_password = EmailTemplate(
subject="Mealie Forgot Password",
header_text="Forgot Password",
message_top="You have requested to reset your password.",
message_bottom="Please click the button above to reset your password.",
subject=self.translator.t("emails.password.subject"),
header_text=self.translator.t("emails.password.header_text"),
message_top=self.translator.t("emails.password.message_top"),
message_bottom=self.translator.t("emails.password.message_bottom"),
button_link=reset_password_url,
button_text="Reset Password",
button_text=self.translator.t("emails.password.button_text"),
)
return self.send_email(address, forgot_password)

def send_invitation(self, address: str, invitation_url: str) -> bool:
invitation = EmailTemplate(
subject="Invitation to join Mealie",
header_text="You're Invited!",
message_top="You have been invited to join Mealie.",
message_bottom="Please click the button above to accept the invitation.",
subject=self.translator.t("emails.invitation.subject"),
header_text=self.translator.t("emails.invitation.header_text"),
message_top=self.translator.t("emails.invitation.message_top"),
message_bottom=self.translator.t("emails.invitation.message_bottom"),
button_link=invitation_url,
button_text="Accept Invitation",
button_text=self.translator.t("emails.invitation.button_text"),
)
return self.send_email(address, invitation)

def send_test_email(self, address: str) -> bool:
test_email = EmailTemplate(
subject="Test Email",
header_text="Test Email",
message_top="This is a test email.",
message_bottom="Please click the button above to test the email.",
button_link="https://www.google.com",
button_text="Test Email",
subject=self.translator.t("emails.test.subject"),
header_text=self.translator.t("emails.test.header_text"),
message_top=self.translator.t("emails.test.message_top"),
message_bottom=self.translator.t("emails.test.message_bottom"),
button_link=self.settings.BASE_URL,
button_text=self.translator.t("emails.test.button_text"),
)
return self.send_email(address, test_email)
4 changes: 2 additions & 2 deletions mealie/services/user_services/password_reset_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ def generate_reset_token(self, email: str) -> SavePasswordResetToken | None:

return self.db.tokens_pw_reset.create(save_token)

def send_reset_email(self, email: str):
def send_reset_email(self, email: str, accept_language: str | None = None):
token_entry = self.generate_reset_token(email)

if token_entry is None:
return None

# Send Email
email_servive = EmailService()
email_servive = EmailService(locale=accept_language)
reset_url = f"{self.settings.BASE_URL}/reset-password/?token={token_entry.token}"

try:
Expand Down
2 changes: 1 addition & 1 deletion tests/unit_tests/services_tests/test_email_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

FAKE_ADDRESS = "[email protected]"

SUBJECTS = {"Mealie Forgot Password", "Invitation to join Mealie", "Test Email"}
SUBJECTS = {"Mealie Forgot Password", "Invitation to join Mealie", "Mealie Test Email"}


class TestEmailSender(ABCEmailSender):
Expand Down

0 comments on commit 60c33b4

Please sign in to comment.