Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: email i18n #2023

Merged
merged 5 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions backend/dto/webhook/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package webhook
type EmailSend struct {
Subject string `json:"subject"` // subject
BodyPlain string `json:"body_plain"` // used for string templates
Body string `json:"body,omitempty"` // used for html templates
Body string `json:"body,omitempty"` // used for HTML templates
ToEmailAddress string `json:"to_email_address"`
DeliveredByHanko bool `json:"delivered_by_hanko"`
AcceptLanguage string `json:"accept_language"` // accept_language header from http request
AcceptLanguage string `json:"accept_language"` // Deprecated. Accept-Language header from HTTP request
Language string `json:"language"` // X-Language header from HTTP request
Type EmailType `json:"type"` // type of the email, currently only "passcode", but other could be added later

Data interface{} `json:"data"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (a ReSendPasscode) Execute(c flowpilot.ExecutionContext) error {
sendParams := services.SendPasscodeParams{
Template: c.Stash().Get(shared.StashPathPasscodeTemplate).String(),
EmailAddress: c.Stash().Get(shared.StashPathEmail).String(),
Language: deps.HttpContext.Request().Header.Get("Accept-Language"),
Language: deps.HttpContext.Request().Header.Get("X-Language"),
}
passcodeResult, err := deps.PasscodeService.SendPasscode(deps.Tx, sendParams)
if err != nil {
Expand All @@ -70,6 +70,7 @@ func (a ReSendPasscode) Execute(c flowpilot.ExecutionContext) error {
ToEmailAddress: sendParams.EmailAddress,
DeliveredByHanko: deps.Cfg.EmailDelivery.Enabled,
AcceptLanguage: sendParams.Language,
Language: sendParams.Language,
Type: webhook.EmailTypePasscode,
Data: webhook.PasscodeData{
ServiceName: deps.Cfg.Service.Name,
Expand Down
3 changes: 2 additions & 1 deletion backend/flow_api/flow/credential_usage/hook_send_passcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (h SendPasscode) Execute(c flowpilot.HookExecutionContext) error {
sendParams := services.SendPasscodeParams{
Template: c.Stash().Get(shared.StashPathPasscodeTemplate).String(),
EmailAddress: c.Stash().Get(shared.StashPathEmail).String(),
Language: deps.HttpContext.Request().Header.Get("Accept-Language"),
Language: deps.HttpContext.Request().Header.Get("X-Language"),
}

passcodeResult, err := deps.PasscodeService.SendPasscode(deps.Tx, sendParams)
Expand All @@ -91,6 +91,7 @@ func (h SendPasscode) Execute(c flowpilot.HookExecutionContext) error {
ToEmailAddress: sendParams.EmailAddress,
DeliveredByHanko: deps.Cfg.EmailDelivery.Enabled,
AcceptLanguage: sendParams.Language,
Language: sendParams.Language,
Type: webhook.EmailTypePasscode,
Data: webhook.PasscodeData{
ServiceName: deps.Cfg.Service.Name,
Expand Down
1 change: 1 addition & 0 deletions backend/handler/passcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ func (h *PasscodeHandler) Init(c echo.Context) error {
ToEmailAddress: email.Address,
DeliveredByHanko: true,
AcceptLanguage: lang,
Language: lang,
Type: webhook.EmailTypePasscode,
Data: webhook.PasscodeData{
ServiceName: h.cfg.Service.Name,
Expand Down
38 changes: 38 additions & 0 deletions backend/mail/locales/passcode.bn.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
login_text:
description: "The sign in content of the text email."
other: "আপনার পরিচয় যাচাই করতে নিচের পাসকোডটি দিন:"
ttl_text:
description: "The length how long the passcode is valid."
other: "পাসকোডটি {{ .TTL }} মিনিটের জন্য বৈধ।"
email_subject_login:
description: ""
other: "{{ .Code }} হল আপনার পাসকোড {{ .ServiceName }} এর জন্য"
subject_email_verification:
description: ""
other: "আপনার ইমেল ঠিকানা যাচাই করতে পাসকোড {{ .Code }} ব্যবহার করুন"
subject_login:
description: ""
other: "আপনার অ্যাকাউন্টে লগইন করতে পাসকোড {{ .Code }} ব্যবহার করুন"
subject_recovery:
description: ""
other: "আপনার অ্যাকাউন্ট পুনরুদ্ধার করতে পাসকোড {{ .Code }} ব্যবহার করুন"
email_verification_text:
description: ""
other: "আপনার ইমেল ঠিকানা যাচাই করতে নিচের পাসকোডটি দিন:"
recovery_text:
description: "The content of the recovery text email."
other: "লগইন স্ক্রীনে নিচের পাসকোডটি দিন:"

subject_email_login_attempted:
description: "Subject for notification about a login attempt."
other: "প্রদত্ত ইমেল ঠিকানা চিহ্নিত হয়নি"
email_login_attempted_text:
description: "Notifies the recipient that either they or someone else attempted to log in to a specific service using an unrecognized email address."
other: "আপনি বা অন্য কেউ {{ .ServiceName }} তে সাইন ইন করার চেষ্টা করেছেন, তবে প্রদত্ত ইমেল ঠিকানা চিহ্নিত হয়নি। দয়া করে আগে একটি অ্যাকাউন্ট তৈরি করুন।"

subject_email_registration_attempted:
description: "Subject for notification about a registration attempt."
other: "প্রদত্ত ইমেল ঠিকানা ইতিমধ্যেই ব্যবহৃত"
email_registration_attempted_text:
description: "Notifies the recipient that either they or someone else attempted to register for a specific service using an email address that is already in use."
other: "আপনি বা অন্য কেউ {{ .ServiceName }} এর জন্য একটি ইমেল নিবন্ধন করার চেষ্টা করেছেন, কিন্তু প্রদত্ত ইমেল ঠিকানা ইতিমধ্যেই নিবন্ধিত। দয়া করে তার পরিবর্তে লগইন করার চেষ্টা করুন।"
39 changes: 39 additions & 0 deletions backend/mail/locales/passcode.de.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
login_text:
description: "The sign in content of the text email."
other: "Geben Sie den folgenden Passcode ein, um Ihre Identität zu überprüfen:"
ttl_text:
description: "The length how long the passcode is valid."
other: "Der Passcode ist {{ .TTL }} Minuten lang gültig."
email_subject_login:
description: ""
other: "{{ .Code }} ist Ihr Passcode für {{ .ServiceName }}"
subject_email_verification:
description: ""
other: "Verwenden Sie den Passcode {{ .Code }}, um Ihre E-Mail-Adresse zu bestätigen"
subject_login:
description: ""
other: "Verwenden Sie den Passcode {{ .Code }}, um sich in Ihr Konto einzuloggen"
subject_recovery:
description: ""
other: "Verwenden Sie den Passcode {{ .Code }}, um Ihr Konto wiederherzustellen"
email_verification_text:
description: ""
other: "Geben Sie den folgenden Passcode ein, um Ihre E-Mail-Adresse zu bestätigen:"
recovery_text:
description: "The content of the recovery text email."
other: "Geben Sie auf Ihrem Anmeldebildschirm den folgenden Passcode ein:"

subject_email_login_attempted:
description: "Subject for notification about a login attempt."
other: "Die angegebene E-Mail-Adresse wird nicht erkannt."
email_login_attempted_text:
description: "Notifies the recipient that either they or someone else attempted to log in to a specific service using an unrecognized email address."
other: "Sie oder eine andere Person haben versucht, sich bei {{ .ServiceName }} anzumelden, aber die angegebene E-Mail-Adresse wird nicht erkannt. Bitte erstellen Sie zunächst ein Konto."

subject_email_registration_attempted:
description: "Subject for notification about a registration attempt."
other: "Die angegebene E-Mail-Adresse ist bereits vergeben."
email_registration_attempted_text:
description: "Notifies the recipient that either they or someone else attempted to register for a specific service using an email address that is already in use."
other: "Sie oder eine andere Person haben versucht, eine E-Mail für {{ .ServiceName }} zu registrieren, aber die angegebene E-Mail-Adresse ist bereits registriert. Bitte versuchen Sie stattdessen, sich anzumelden."

36 changes: 36 additions & 0 deletions backend/mail/locales/passcode.fr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
login_text:
description: "The sign in content of the text email."
other: "Entrez le code d'accès suivant pour vérifier votre identité :"
ttl_text:
description: "The length how long the passcode is valid."
other: "Le code d'accès est valable pendant {{ .TTL }} minutes."
email_subject_login:
description: ""
other: "{{ .Code }} est votre code d'accès pour {{ .ServiceName }}"
subject_email_verification:
description: ""
other: "Utilisez le code d'accès {{ .Code }} pour vérifier votre adresse e-mail"
subject_login:
description: ""
other: "Utilisez le code d'accès {{ .Code }} pour vous connecter à votre compte"
subject_recovery:
description: ""
other: "Utilisez le code d'accès {{ .Code }} pour récupérer votre compte"
email_verification_text:
description: ""
other: "Entrez le code d'accès suivant pour vérifier votre adresse e-mail :"
recovery_text:
description: "The content of the recovery text email."
other: "Entrez le code d'accès suivant sur votre écran de connexion :"
subject_email_login_attempted:
description: "Subject for notification about a login attempt."
other: "L'adresse e-mail fournie n'est pas reconnue"
email_login_attempted_text:
description: "Notifies the recipient that either they or someone else attempted to log in to a specific service using an unrecognized email address."
other: "Vous ou quelqu'un d'autre avez essayé de vous connecter à {{ .ServiceName }}, mais l'adresse e-mail fournie n'a pas été reconnue. Veuillez d'abord créer un compte."
subject_email_registration_attempted:
description: "Subject for notification about a registration attempt."
other: "L'adresse e-mail fournie est déjà utilisée"
email_registration_attempted_text:
description: "Notifies the recipient that either they or someone else attempted to register for a specific service using an email address that is already in use."
other: "Vous ou quelqu'un d'autre avez tenté d'enregistrer un e-mail pour {{ .ServiceName }}, mais l'adresse e-mail fournie est déjà enregistrée. Veuillez essayer de vous connecter à la place."
36 changes: 36 additions & 0 deletions backend/mail/locales/passcode.it.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
login_text:
description: "The sign in content of the text email."
other: "Inserisci il seguente codice di accesso per verificare la tua identità:"
ttl_text:
description: "The length how long the passcode is valid."
other: "Il codice di accesso è valido per {{ .TTL }} minuti."
email_subject_login:
description: ""
other: "{{ .Code }} è il tuo codice di accesso per {{ .ServiceName }}"
subject_email_verification:
description: ""
other: "Usa il codice di accesso {{ .Code }} per verificare il tuo indirizzo e-mail"
subject_login:
description: ""
other: "Usa il codice di accesso {{ .Code }} per accedere al tuo account"
subject_recovery:
description: ""
other: "Usa il codice di accesso {{ .Code }} per recuperare il tuo account"
email_verification_text:
description: ""
other: "Inserisci il seguente codice di accesso per verificare il tuo indirizzo e-mail:"
recovery_text:
description: "The content of the recovery text email."
other: "Inserisci il seguente codice di accesso nella tua schermata di login:"
subject_email_login_attempted:
description: "Subject for notification about a login attempt."
other: "Indirizzo e-mail fornito non riconosciuto"
email_login_attempted_text:
description: "Notifies the recipient that either they or someone else attempted to log in to a specific service using an unrecognized email address."
other: "Tu o qualcun altro avete provato ad accedere a {{ .ServiceName }}, ma l'indirizzo e-mail fornito non è stato riconosciuto. Per favore, crea prima un account."
subject_email_registration_attempted:
description: "Subject for notification about a registration attempt."
other: "Indirizzo e-mail fornito già in uso"
email_registration_attempted_text:
description: "Notifies the recipient that either they or someone else attempted to register for a specific service using an email address that is already in use."
other: "Tu o qualcun altro avete provato a registrare un'e-mail per {{ .ServiceName }}, ma l'indirizzo e-mail fornito è già registrato. Per favore, prova a fare il login invece."
38 changes: 38 additions & 0 deletions backend/mail/locales/passcode.pt-BR.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
login_text:
description: "The sign in content of the text email."
other: "Digite o seguinte código de acesso para verificar sua identidade:"
ttl_text:
description: "The length how long the passcode is valid."
other: "O código de acesso é válido por {{ .TTL }} minutos."
email_subject_login:
description: ""
other: "{{ .Code }} é o seu código de acesso para {{ .ServiceName }}"
subject_email_verification:
description: ""
other: "Use o código de acesso {{ .Code }} para verificar o seu endereço de e-mail"
subject_login:
description: ""
other: "Use o código de acesso {{ .Code }} para fazer login na sua conta"
subject_recovery:
description: ""
other: "Use o código de acesso {{ .Code }} para recuperar sua conta"
email_verification_text:
description: ""
other: "Digite o seguinte código de acesso para verificar o seu endereço de e-mail:"
recovery_text:
description: "The content of the recovery text email."
other: "Digite o seguinte código de acesso na tela de login:"

subject_email_login_attempted:
description: "Subject for notification about a login attempt."
other: "Endereço de e-mail fornecido não reconhecido"
email_login_attempted_text:
description: "Notifies the recipient that either they or someone else attempted to log in to a specific service using an unrecognized email address."
other: "Você ou outra pessoa tentou fazer login no {{ .ServiceName }}, mas o endereço de e-mail fornecido não foi reconhecido. Por favor, crie uma conta primeiro."

subject_email_registration_attempted:
description: "Subject for notification about a registration attempt."
other: "Endereço de e-mail fornecido já está em uso"
email_registration_attempted_text:
description: "Notifies the recipient that either they or someone else attempted to register for a specific service using an email address that is already in use."
other: "Você ou outra pessoa tentou registrar um e-mail para {{ .ServiceName }}, mas o endereço de e-mail fornecido já está registrado. Por favor, tente fazer login em vez disso."
8 changes: 8 additions & 0 deletions frontend/elements/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ Translations are currently available for the following languages:
- "de" - German
- "en" - English
- "fr" - French
- "it" - Italian
- "ptBR" - Brazilian Portuguese
- "zh" - Simplified Chinese

Expand Down Expand Up @@ -706,6 +707,13 @@ Markup:
<hanko-auth lang="symbols"></hanko-auth>
```

### Translation of outgoing Hanko emails

If you use Hanko Elements the language supplied to the `lang` attribute of any of the components is also used to convey
to the Hanko API the language to use for outgoing emails. If you have disabled email delivery through Hanko and
configured a webhook for the `email.send` event, the value for the `lang` attribute is reflected in the JWT payload of
the token contained in the webhook request in the `language` claim.

## Experimental Features

### Conditional Mediation / Autofill assisted Requests
Expand Down
10 changes: 7 additions & 3 deletions frontend/elements/src/components/wrapper/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ interface Props extends h.JSX.HTMLAttributes<HTMLElement> {
}

const Container = forwardRef<HTMLElement>((props: Props, ref) => {
const { lang } = useContext(AppContext);
const { lang, hanko, setHanko } = useContext(AppContext);
const { setLang } = useContext(TranslateContext);

useEffect(() => {
setLang(lang);
}, [lang, setLang]);
setLang(lang.replace(/[-]/, ""));
setHanko((hanko) => {
hanko.setLang(lang);
return hanko;
});
}, [hanko, lang, setHanko, setLang]);

return (
<section part={"container"} className={styles.container} ref={ref}>
Expand Down
8 changes: 8 additions & 0 deletions frontend/elements/src/contexts/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ interface UIState {

interface Context {
hanko: Hanko;
setHanko: StateUpdater<Hanko>;
page: h.JSX.Element;
setPage: StateUpdater<h.JSX.Element>;
init: (compName: ComponentName) => void;
Expand Down Expand Up @@ -179,6 +180,11 @@ const AppProvider = ({
fallbackLanguage,
} = globalOptions;

// Without this, the initial "lang" attribute value sometimes appears to not
// be set properly. This results in a wrong X-Language header value being sent
// to the API and hence in outgoing emails translated in the wrong language.
hanko.setLang(lang?.toString() || fallbackLanguage);

const ref = useRef<HTMLElement>(null);

const storageKeyLastLogin = useMemo(
Expand All @@ -201,6 +207,7 @@ const AppProvider = ({

const initComponent = useMemo(() => <InitPage />, []);
const [page, setPage] = useState<h.JSX.Element>(initComponent);
const [, setHanko] = useState<Hanko>(hanko);
const [lastLogin, setLastLogin] = useState<LastLogin>();
const [uiState, setUIState] = useState<UIState>({
email: prefilledEmail,
Expand Down Expand Up @@ -598,6 +605,7 @@ const AppProvider = ({
setSucceededAction,
uiState,
hanko,
setHanko,
lang: lang?.toString() || fallbackLanguage,
prefilledEmail,
prefilledUsername,
Expand Down
2 changes: 1 addition & 1 deletion frontend/elements/src/example.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
<option value="en" selected>English</option>
<option value="fr">French</option>
<option value="it">Italian</option>
<option value="ptBR">Brazilian Portuguese</option>
<option value="pt-BR">Brazilian Portuguese</option>
<option value="zh">Chinese</option>
</select>
</nav>
Expand Down
8 changes: 8 additions & 0 deletions frontend/frontend-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,14 @@ hanko.onUserDeleted(() => {

Please Take a look into the [docs](https://teamhanko.github.io/hanko/jsdoc/hanko-frontend-sdk/) for more details.

### Translation of outgoing emails

If you use the main `Hanko` client provided by the Frontend SDK, you can use the `lang` parameter in the options when
instantiating the client to configure the language that is used to convey to the Hanko API the
language to use for outgoing emails. If you have disabled email delivery through Hanko and configured a webhook for the
`email.send` event, the value for the `lang` parameter is reflected in the JWT payload of the token contained in the
webhook request in the "Language" claim.

## Bugs

Found a bug? Please report on our [GitHub](https://github.com/teamhanko/hanko/issues) page.
Expand Down
Loading
Loading