diff --git a/backend/LexBoxApi/Services/EmailService.cs b/backend/LexBoxApi/Services/EmailService.cs index 2093081a5c..c634f1329b 100644 --- a/backend/LexBoxApi/Services/EmailService.cs +++ b/backend/LexBoxApi/Services/EmailService.cs @@ -43,7 +43,7 @@ public async Task SendForgotPasswordEmail(string emailAddress) "Login", new { jwt, returnTo = "/resetPassword" }); ArgumentException.ThrowIfNullOrEmpty(forgotLink); - await RenderEmail(email, new ForgotPasswordEmail(user.Name, forgotLink)); + await RenderEmail(email, new ForgotPasswordEmail(user.Name, forgotLink), user.LocalizationCode); await SendEmailAsync(email); } @@ -72,14 +72,14 @@ public async Task SendVerifyAddressEmail(User user, string? newEmail = null) "Login", new { jwt, returnTo = $"/user?emailResult={queryParam}", email = newEmail ?? user.Email, }); ArgumentException.ThrowIfNullOrEmpty(verifyLink); - await RenderEmail(email, new VerifyAddressEmail(user.Name, verifyLink, !string.IsNullOrEmpty(newEmail))); + await RenderEmail(email, new VerifyAddressEmail(user.Name, verifyLink, !string.IsNullOrEmpty(newEmail)), user.LocalizationCode); await SendEmailAsync(email); } public async Task SendPasswordChangedEmail(User user) { var email = StartUserEmail(user); - await RenderEmail(email, new PasswordChangedEmail(user.Name)); + await RenderEmail(email, new PasswordChangedEmail(user.Name), user.LocalizationCode); await SendEmailAsync(email); } @@ -88,7 +88,7 @@ public async Task SendCreateProjectRequestEmail(LexAuthUser user, CreateProjectI var email = new MimeMessage(); email.To.Add(new MailboxAddress("Admin", _emailConfig.CreateProjectEmailDestination)); await RenderEmail(email, - new CreateProjectRequestEmail("Admin", new CreateProjectRequestUser(user.Name, user.Email), projectInput)); + new CreateProjectRequestEmail("Admin", new CreateProjectRequestUser(user.Name, user.Email), projectInput), "en"); await SendEmailAsync(email); } @@ -119,7 +119,7 @@ private async Task SendEmailAsync(MimeMessage message) private record RenderResult(string Subject, string Html); - private async Task RenderEmail(MimeMessage message, T parameters) where T : EmailTemplateBase + private async Task RenderEmail(MimeMessage message, T parameters, string recipientLocale) where T : EmailTemplateBase { using var activity = LexBoxActivitySource.Get().StartActivity(); activity?.AddTag("app.email.template", typeof(T).Name); @@ -127,6 +127,8 @@ private async Task RenderEmail(MimeMessage message, T parameters) where T : E var httpClient = clientFactory.CreateClient(); httpClient.BaseAddress = new Uri("http://" + _emailConfig.EmailRenderHost); parameters.BaseUrl = _emailConfig.BaseUrl; + // Uses TryAddWithoutValidation to work around: https://github.com/cibernox/precompile-intl-runtime/issues/45 + httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept-Language", $"{recipientLocale};q=1.0"); var response = await httpClient.PostAsJsonAsync("email", parameters, jsonSerializerOptions); response.EnsureSuccessStatusCode(); var renderResult = await response.Content.ReadFromJsonAsync(); diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index 5aad4c32d7..6c00fe1fed 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -22,6 +22,7 @@ declare global { interface Locals { client: Client; getUser: (() => LexAuthUser | null); + requestedLocale?: string; } interface Error { diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index 16156bcf61..f5f2ea5b00 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -1,11 +1,14 @@ +import { loadI18n } from '$lib/i18n'; import { AUTH_COOKIE_NAME, getUser, isAuthn } from '$lib/user' import { apiVersion } from '$lib/util/version'; import { redirect, type Handle, type HandleFetch, type HandleServerError, type ResolveOptions } from '@sveltejs/kit' import { ensureErrorIsTraced, traceRequest, traceFetch } from '$lib/otel/otel.server' +import { availableLocales } from '$locales'; import { env } from '$env/dynamic/private'; import { getErrorMessage, validateFetchResponse } from './hooks.shared'; -import {setViewMode} from './routes/(authenticated)/shared'; +import { setViewMode } from './routes/(authenticated)/shared'; import * as setCookieParser from 'set-cookie-parser'; +import { getLocaleFromAcceptLanguageHeader } from 'svelte-intl-precompile'; import { AUTHENTICATED_ROOT, UNAUTHENTICATED_ROOT } from './routes'; const PUBLIC_ROUTE_ROOTS = [ @@ -24,6 +27,10 @@ export const handle: Handle = ({ event, resolve }) => { console.log(`HTTP request: ${event.request.method} ${event.request.url}`); event.locals.getUser = () => getUser(event.cookies); return traceRequest(event, async () => { + const user = event.locals.getUser(); + const requestedLocale = user?.locale + ?? getLocaleFromAcceptLanguageHeader(event.request.headers.get('Accept-Language'), availableLocales); + await loadI18n(requestedLocale); const options: ResolveOptions = { filterSerializedResponseHeaders: () => true, diff --git a/frontend/src/routes/+layout.server.ts b/frontend/src/routes/+layout.server.ts index d12166b8ca..974e6d9039 100644 --- a/frontend/src/routes/+layout.server.ts +++ b/frontend/src/routes/+layout.server.ts @@ -2,12 +2,9 @@ import { APP_VERSION, apiVersion } from '$lib/util/version'; import type { LayoutServerLoadEvent } from './$types' import { USER_LOAD_KEY } from '$lib/user'; -import { availableLocales } from '$locales'; -import { getLocaleFromAcceptLanguageHeader } from 'svelte-intl-precompile'; import { getRootTraceparent } from '$lib/otel/otel.server' -export async function load({ locals, depends, fetch, request }: LayoutServerLoadEvent) { - const requestLang = getLocaleFromAcceptLanguageHeader(request.headers.get('Accept-Language'), availableLocales); +export async function load({ locals, depends, fetch }: LayoutServerLoadEvent) { const user = locals.getUser(); const traceParent = getRootTraceparent() @@ -20,7 +17,7 @@ export async function load({ locals, depends, fetch, request }: LayoutServerLoad } return { user, - locale: user?.locale ?? requestLang, + locale: user?.locale ?? locals.requestedLocale, traceParent, serverVersion: APP_VERSION, apiVersion: apiVersion.value diff --git a/frontend/src/routes/+layout.ts b/frontend/src/routes/+layout.ts index 6a530aa526..07a81bfcd1 100644 --- a/frontend/src/routes/+layout.ts +++ b/frontend/src/routes/+layout.ts @@ -1,10 +1,13 @@ import type { LayoutLoadEvent } from './$types'; +import { browser } from '$app/environment'; import { loadI18n } from '$lib/i18n'; //setting this to false can help diagnose requests to the api as you can see them in the browser instead of sveltekit export const ssr = true; export async function load(event: LayoutLoadEvent) { - await loadI18n(event.data.locale); + if (browser) { // i18n is initialized on the server in hooks.server.ts + await loadI18n(event.data.locale); + } return event.data; }