From 5023a53980eb352c9d52bc6ead86310135d934d0 Mon Sep 17 00:00:00 2001
From: Lennart Fleischmann <67686424+lfleischmann@users.noreply.github.com>
Date: Mon, 13 Jan 2025 12:58:43 +0100
Subject: [PATCH] feat: email i18n
---
backend/dto/webhook/email.go | 5 ++-
.../action_resend_passcode.go | 3 +-
.../credential_usage/hook_send_passcode.go | 3 +-
backend/handler/passcode.go | 1 +
backend/mail/locales/passcode.bn.yaml | 38 ++++++++++++++++++
backend/mail/locales/passcode.de.yaml | 39 +++++++++++++++++++
backend/mail/locales/passcode.fr.yaml | 36 +++++++++++++++++
backend/mail/locales/passcode.it.yaml | 36 +++++++++++++++++
backend/mail/locales/passcode.pt-BR.yaml | 38 ++++++++++++++++++
frontend/elements/README.md | 8 ++++
.../src/components/wrapper/Container.tsx | 10 +++--
.../elements/src/contexts/AppProvider.tsx | 8 ++++
frontend/elements/src/example.html | 2 +-
frontend/frontend-sdk/README.md | 8 ++++
frontend/frontend-sdk/src/Hanko.ts | 23 +++++++++++
.../frontend-sdk/src/lib/client/HttpClient.ts | 7 ++++
.../frontend-sdk/src/lib/flow-api/Flow.ts | 4 --
.../tests/lib/client/HttpClient.spec.ts | 4 +-
18 files changed, 259 insertions(+), 14 deletions(-)
create mode 100644 backend/mail/locales/passcode.bn.yaml
create mode 100644 backend/mail/locales/passcode.de.yaml
create mode 100644 backend/mail/locales/passcode.fr.yaml
create mode 100644 backend/mail/locales/passcode.it.yaml
create mode 100644 backend/mail/locales/passcode.pt-BR.yaml
diff --git a/backend/dto/webhook/email.go b/backend/dto/webhook/email.go
index 40d49237a..7d33e4886 100644
--- a/backend/dto/webhook/email.go
+++ b/backend/dto/webhook/email.go
@@ -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"`
diff --git a/backend/flow_api/flow/credential_usage/action_resend_passcode.go b/backend/flow_api/flow/credential_usage/action_resend_passcode.go
index b6e33a841..b75c31d4e 100644
--- a/backend/flow_api/flow/credential_usage/action_resend_passcode.go
+++ b/backend/flow_api/flow/credential_usage/action_resend_passcode.go
@@ -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 {
@@ -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,
diff --git a/backend/flow_api/flow/credential_usage/hook_send_passcode.go b/backend/flow_api/flow/credential_usage/hook_send_passcode.go
index 085eb25cb..f16494248 100644
--- a/backend/flow_api/flow/credential_usage/hook_send_passcode.go
+++ b/backend/flow_api/flow/credential_usage/hook_send_passcode.go
@@ -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)
@@ -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,
diff --git a/backend/handler/passcode.go b/backend/handler/passcode.go
index ce6e0bc0d..53e8a28e3 100644
--- a/backend/handler/passcode.go
+++ b/backend/handler/passcode.go
@@ -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,
diff --git a/backend/mail/locales/passcode.bn.yaml b/backend/mail/locales/passcode.bn.yaml
new file mode 100644
index 000000000..6711ac284
--- /dev/null
+++ b/backend/mail/locales/passcode.bn.yaml
@@ -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 }} এর জন্য একটি ইমেল নিবন্ধন করার চেষ্টা করেছেন, কিন্তু প্রদত্ত ইমেল ঠিকানা ইতিমধ্যেই নিবন্ধিত। দয়া করে তার পরিবর্তে লগইন করার চেষ্টা করুন।"
diff --git a/backend/mail/locales/passcode.de.yaml b/backend/mail/locales/passcode.de.yaml
new file mode 100644
index 000000000..79f8ec58f
--- /dev/null
+++ b/backend/mail/locales/passcode.de.yaml
@@ -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."
+
diff --git a/backend/mail/locales/passcode.fr.yaml b/backend/mail/locales/passcode.fr.yaml
new file mode 100644
index 000000000..7679e473c
--- /dev/null
+++ b/backend/mail/locales/passcode.fr.yaml
@@ -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."
diff --git a/backend/mail/locales/passcode.it.yaml b/backend/mail/locales/passcode.it.yaml
new file mode 100644
index 000000000..36c9e9dbc
--- /dev/null
+++ b/backend/mail/locales/passcode.it.yaml
@@ -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."
diff --git a/backend/mail/locales/passcode.pt-BR.yaml b/backend/mail/locales/passcode.pt-BR.yaml
new file mode 100644
index 000000000..625ee5359
--- /dev/null
+++ b/backend/mail/locales/passcode.pt-BR.yaml
@@ -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."
diff --git a/frontend/elements/README.md b/frontend/elements/README.md
index 23eae175e..2d4f48ea9 100644
--- a/frontend/elements/README.md
+++ b/frontend/elements/README.md
@@ -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
@@ -706,6 +707,13 @@ Markup:
```
+### 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
diff --git a/frontend/elements/src/components/wrapper/Container.tsx b/frontend/elements/src/components/wrapper/Container.tsx
index 999593db5..5d86f56f8 100644
--- a/frontend/elements/src/components/wrapper/Container.tsx
+++ b/frontend/elements/src/components/wrapper/Container.tsx
@@ -10,12 +10,16 @@ interface Props extends h.JSX.HTMLAttributes {
}
const Container = forwardRef((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 (
diff --git a/frontend/elements/src/contexts/AppProvider.tsx b/frontend/elements/src/contexts/AppProvider.tsx
index 92ea3b3a5..caecc2515 100644
--- a/frontend/elements/src/contexts/AppProvider.tsx
+++ b/frontend/elements/src/contexts/AppProvider.tsx
@@ -128,6 +128,7 @@ interface UIState {
interface Context {
hanko: Hanko;
+ setHanko: StateUpdater;
page: h.JSX.Element;
setPage: StateUpdater;
init: (compName: ComponentName) => void;
@@ -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(null);
const storageKeyLastLogin = useMemo(
@@ -201,6 +207,7 @@ const AppProvider = ({
const initComponent = useMemo(() => , []);
const [page, setPage] = useState(initComponent);
+ const [, setHanko] = useState(hanko);
const [lastLogin, setLastLogin] = useState();
const [uiState, setUIState] = useState({
email: prefilledEmail,
@@ -598,6 +605,7 @@ const AppProvider = ({
setSucceededAction,
uiState,
hanko,
+ setHanko,
lang: lang?.toString() || fallbackLanguage,
prefilledEmail,
prefilledUsername,
diff --git a/frontend/elements/src/example.html b/frontend/elements/src/example.html
index f150fb94a..82be327b6 100644
--- a/frontend/elements/src/example.html
+++ b/frontend/elements/src/example.html
@@ -135,7 +135,7 @@
-
+
diff --git a/frontend/frontend-sdk/README.md b/frontend/frontend-sdk/README.md
index d2016b811..4776b659e 100644
--- a/frontend/frontend-sdk/README.md
+++ b/frontend/frontend-sdk/README.md
@@ -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.
diff --git a/frontend/frontend-sdk/src/Hanko.ts b/frontend/frontend-sdk/src/Hanko.ts
index 5b04a0b24..10b08a86d 100644
--- a/frontend/frontend-sdk/src/Hanko.ts
+++ b/frontend/frontend-sdk/src/Hanko.ts
@@ -19,6 +19,12 @@ import { SessionClient } from "./lib/client/SessionClient";
* @property {string=} cookieDomain - The domain where the cookie set from the SDK is available. Defaults to the domain of the page where the cookie was created.
* @property {string=} cookieSameSite - Specify whether/when cookies are sent with cross-site requests. Defaults to "lax".
* @property {string=} localStorageKey - The prefix / name of the local storage keys. Defaults to "hanko"
+ * @property {string=} lang - Used to convey the preferred language to the API, e.g. for translating outgoing emails.
+ * It is transmitted to the API in a custom header (X-Language).
+ * Should match one of the supported languages ("bn", "de", "en", "fr", "it, "pt-BR", "zh")
+ * if email delivery by Hanko is enabled. If email delivery by Hanko is disabled and the
+ * relying party configures a webhook for the "email.send" event, then the set language is
+ * reflected in the payload of the token contained in the webhook request.
*/
export interface HankoOptions {
timeout?: number;
@@ -26,6 +32,7 @@ export interface HankoOptions {
cookieDomain?: string;
cookieSameSite?: CookieSameSite;
localStorageKey?: string;
+ lang?: string;
}
/**
@@ -70,6 +77,9 @@ class Hanko extends Listener {
if (options?.cookieSameSite !== undefined) {
opts.cookieSameSite = options.cookieSameSite;
}
+ if (options?.lang !== undefined) {
+ opts.lang = options.lang;
+ }
this.api = api;
/**
@@ -118,6 +128,18 @@ class Hanko extends Listener {
*/
this.flow = new Flow(api, opts);
}
+
+ /**
+ * Sets the preferred language on the underlying sub-clients. The clients'
+ * base HttpClient uses this language to transmit an X-Language header to the
+ * API which is then used to e.g. translate outgoing emails.
+ *
+ * @public
+ * @param lang {string} - The preferred language to convey to the API.
+ */
+ setLang(lang: string) {
+ this.flow.client.lang = lang;
+ }
}
// eslint-disable-next-line require-jsdoc
@@ -127,6 +149,7 @@ export interface InternalOptions {
cookieDomain?: string;
cookieSameSite?: CookieSameSite;
localStorageKey: string;
+ lang?: string;
}
export { Hanko };
diff --git a/frontend/frontend-sdk/src/lib/client/HttpClient.ts b/frontend/frontend-sdk/src/lib/client/HttpClient.ts
index 7cdac012d..d8690a902 100644
--- a/frontend/frontend-sdk/src/lib/client/HttpClient.ts
+++ b/frontend/frontend-sdk/src/lib/client/HttpClient.ts
@@ -115,12 +115,15 @@ class Response {
* @property {string} cookieName - The name of the session cookie set from the SDK.
* @property {string=} cookieDomain - The domain where cookie set from the SDK is available. Defaults to the domain of the page where the cookie was created.
* @property {string} localStorageKey - The prefix / name of the local storage keys.
+ * @property {string} lang - The language used by the client(s) to convey to the Hanko API the language to use -
+ * e.g. for translating outgoing emails - in a custom header (X-Language).
*/
export interface HttpClientOptions {
timeout: number;
cookieName: string;
cookieDomain?: string;
localStorageKey: string;
+ lang?: string;
}
/**
@@ -143,6 +146,7 @@ class HttpClient {
sessionState: SessionState;
dispatcher: Dispatcher;
cookie: Cookie;
+ lang: string;
// eslint-disable-next-line require-jsdoc
constructor(api: string, options: HttpClientOptions) {
@@ -151,6 +155,7 @@ class HttpClient {
this.sessionState = new SessionState({ ...options });
this.dispatcher = new Dispatcher({ ...options });
this.cookie = new Cookie({ ...options });
+ this.lang = options.lang;
}
// eslint-disable-next-line require-jsdoc
@@ -159,11 +164,13 @@ class HttpClient {
const url = this.api + path;
const timeout = this.timeout;
const bearerToken = this.cookie.getAuthCookie();
+ const lang = this.lang;
return new Promise(function (resolve, reject) {
xhr.open(options.method, url, true);
xhr.setRequestHeader("Accept", "application/json");
xhr.setRequestHeader("Content-Type", "application/json");
+ xhr.setRequestHeader("X-Language", lang);
if (bearerToken) {
xhr.setRequestHeader("Authorization", `Bearer ${bearerToken}`);
diff --git a/frontend/frontend-sdk/src/lib/flow-api/Flow.ts b/frontend/frontend-sdk/src/lib/flow-api/Flow.ts
index 813afe638..7fca051dd 100644
--- a/frontend/frontend-sdk/src/lib/flow-api/Flow.ts
+++ b/frontend/frontend-sdk/src/lib/flow-api/Flow.ts
@@ -4,17 +4,13 @@ import { Action } from "./types/action";
import { FetchNextState, FlowPath, Handlers } from "./types/state-handling";
import { HankoError } from "../Errors";
-type MaybePromise = T | Promise;
-
type ExtendedHandlers = Handlers & { onError?: (e: unknown) => any };
-type GetInitState = (flow: Flow) => MaybePromise | null>;
// eslint-disable-next-line require-jsdoc
class Flow extends Client {
public async init(
initPath: FlowPath,
handlers: ExtendedHandlers,
- // getInitState: GetInitState = () => this.fetchNextState(initPath),
): Promise {
const fetchNextState: FetchNextState = async (href: string, body?: any) => {
try {
diff --git a/frontend/frontend-sdk/tests/lib/client/HttpClient.spec.ts b/frontend/frontend-sdk/tests/lib/client/HttpClient.spec.ts
index ec8c00472..b4d13c642 100644
--- a/frontend/frontend-sdk/tests/lib/client/HttpClient.spec.ts
+++ b/frontend/frontend-sdk/tests/lib/client/HttpClient.spec.ts
@@ -49,7 +49,7 @@ describe("httpClient._fetch()", () => {
"Content-Type",
"application/json",
);
- expect(xhr.setRequestHeader).toHaveBeenCalledTimes(2);
+ expect(xhr.setRequestHeader).toHaveBeenCalledTimes(3);
expect(xhr.open).toHaveBeenNthCalledWith(
1,
"GET",
@@ -73,7 +73,7 @@ describe("httpClient._fetch()", () => {
"Authorization",
`Bearer ${jwt}`,
);
- expect(xhr.setRequestHeader).toHaveBeenCalledTimes(3);
+ expect(xhr.setRequestHeader).toHaveBeenCalledTimes(4);
});
it("should handle onerror", async () => {