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

Add i18n support to mail templates #834

Closed
landerss1 opened this issue Nov 18, 2020 · 12 comments · Fixed by #1859
Closed

Add i18n support to mail templates #834

landerss1 opened this issue Nov 18, 2020 · 12 comments · Fixed by #1859
Labels
feat New feature or request. help wanted We are looking for help on this one.

Comments

@landerss1
Copy link
Contributor

Is your feature request related to a problem? Please describe.

Currently, it's not possible to have mail templates for different languages.

Describe the solution you'd like

Make the Recovery Flow with Link Method i18n-aware.

Describe alternatives you've considered

The Complete Recovery Flow with Link Method REST endpoint should accept and honor the standard Accept-Language HTTP header and use it to select the appropriate mail template.

Additional context

N/A

@aeneasr aeneasr added the feat New feature or request. label Dec 6, 2020
@aeneasr aeneasr added this to the v0.7.0-alpha.1 milestone Dec 6, 2020
@aeneasr
Copy link
Member

aeneasr commented Dec 6, 2020

Sorry for the late reply - we have this on the radar but currently with a low priority. We want to avoid to have translations in ORY Kratos itself as those are always hard to get right. Instead, we think that mounting a directory with (translated) mail templates is smarter. What do you think?

@landerss1
Copy link
Contributor Author

Yes, that's the idea I'm proposing. Use the Accept-Language http header to select the right template. It's already possible to mount your own e-mail templates, so all that's needed is that the template functionality add the language extension to the name of the template to get the right one, so it should be a very easy fix.

@aeneasr aeneasr added the help wanted We are looking for help on this one. label Dec 9, 2020
@aeneasr
Copy link
Member

aeneasr commented Dec 9, 2020

Right, so the idea would be to add a language-dependent variable around here:

https://github.com/ory/kratos/blob/master/courier/template/verification_valid.go#L29

I imagine something like:

if language != nil {
	return loadTextTemplate(filepath.Join(t.c.CourierTemplatesRoot(), "verification/valid/"+language.Locale()+"/email.subject.gotmpl"), t.m)
}
return loadTextTemplate(filepath.Join(t.c.CourierTemplatesRoot(), "verification/valid/email.subject.gotmpl"), t.m)

One problem will be getting the language into this function as the struct currently does not receive the http.Request and can thus not parse the Accept-Language header.

Another problem is that these emails sometimes are not generated as part of an user-initiated HTTP Request. One such example is sending an account recovery to a user as an administrator. Here, the Accept-Language header is completely irrelevant because it is the header of the admin, not the user.

This means that we would need some other way of identifying the user locale - probably via his/her traits. I don't really know yet what this would look like without compromising the identity model. Maybe a special trait? But how do we get that in this function then here?

So there are some things to figure out, and I would really appreciate some help on this! :)

@landerss1
Copy link
Contributor Author

Then, maybe a better solution would be to introduce a special request parameter, like lang, using standard Golang language tags? At least that would solve the problem where the admin initiates the request. Then remains the problem of passing the parameter on to the template function. Messing with the identity model and trying to standardize the traits seems like a complex approach.

@aeneasr
Copy link
Member

aeneasr commented Dec 11, 2020

Where would that request parameter be sent? What would be different from the Accept header?

I think a locale for the user is something ORY Kratos should somehow support.

@zepatrik
Copy link
Member

What about making the identity's traits available to the template? One can then determine the language however they want (e.g. Accept header during signup, form input, ...). In the template this can then be used to select the right text. We could even provide a function that loads a file dynamically, similar to https://stackoverflow.com/questions/20716726/call-other-templates-with-dynamic-name
This would allow to do more than just different languages BTW. You could store the utm_source in the traits and then greet people coming from different sources differently. Or maybe send different content based on a role a user has.

@aeneasr
Copy link
Member

aeneasr commented Dec 11, 2020

Big brain, @zepatrik ! This would then mean that the template has the translation, not the file system:

if (trait.bla=="DE") {
<asdf>
} else {
<bdasd>
}

(with the go specific templating instructions of course).

@landerss1
Copy link
Contributor Author

landerss1 commented Dec 14, 2020

@zepatrik I like the idea of having the traits available in the template. That would allow for further customization of emails based on the identity. However, having also different languages in the template itself could lead to problems maintaining the template, especially if you support many languages and use html to spice up the templates.

@aeneasr So, returning to the issue of using http header or request parameter, I had a very quick look (hope I'm not missing anything relevant) at the code, and starting at

if err := s.d.LinkSender().SendRecoveryLink(r.Context(), req, identity.VerifiableAddressTypeEmail, body.Body.Email); err != nil {
, you do have access to the http request and can easily put the header or query parameter in the context that is passed along, all the way to the QueueEmail method:
func (m *Courier) QueueEmail(ctx context.Context, t EmailTemplate) (uuid.UUID, error) {
Here, you would have to make the header or parameter available to the EmailSubject and EmailBody methods, which would need some minor re-design. So, it should be possible to propagate the header or parameter fairly easy to the template?

I haven't looked at the other scenarios, such as the verification flow, but I assume they are similar?

So, to sum it up, the ideal solution, in my mind, would be to have both solutions, language header or parameter controlling the template to select, and traits available to customize the content in the template, like greeting with name of the receiver, etc.

@zepatrik
Copy link
Member

Hm right, but you can add the header to the traits yourself on signup/login/... In case of the admin API there would be no such language header, or you would have to set it accordingly.
The problem is that some people are going to use the header, but others might use some other way to decide which language to use.
To make the templates more manageable, I propose adding some way to load nested templates from multiple files.

@landerss1
Copy link
Contributor Author

Our users don't perform self-service sign-up, they are all created using the Admin API, but yes, your right, there will be different ways people would want to handle this. However, using the traits would require the need to standardize the traits and as @aeneasr pointed out above, that could potentially compromise the identity model. However, if you're willing to dig into the model, the solution would be quite transparent, which would be nice for the sake of solving this use-case.

@zepatrik
Copy link
Member

I don't get why this would compromise the identity model. I would like to do:

{{ define "fair_customer_greeting" }}
Hi {{ gjson .Traits "name" }},
it was very nice to meet you on the fair. Thanks for signing up for our product. Click the link below to verify your email address.
{{ end }}

{{ define "normal_customer_greeting" }}
Hi {{ gjson .Traits "name" }},
Thanks for signing up for our product. Click the link below to verify your email address.
{{ end }}

{{ if (eq (gjson .Traits "signup_utm_source") "fair") }}
{{ template "fair_customer_greeting" . }}
{{ else }}
{{ template "normal_customer_greeting" . }}
{{ end }}

and set the signup_utm_source as a hidden field during signup.

Or set the trait lang to the header and then use it in your template accordingly. Wouldn't this solve your problem and many more to come?

@landerss1
Copy link
Contributor Author

It would solve our current problem, yes 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat New feature or request. help wanted We are looking for help on this one.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants