Skip to content

Commit 43c8010

Browse files
authored
Merge pull request #7811 from logto-io/charles-log-12162-core-update-email-template-to-support-ui_locales-param
feat(core,toolkit): add ui_locales support in email template variables
1 parent 2f2f384 commit 43c8010

File tree

8 files changed

+45
-16
lines changed

8 files changed

+45
-16
lines changed

packages/core/src/libraries/passcode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const passcodeMaxTryCount = 10;
2828

2929
export type PasscodeLibrary = ReturnType<typeof createPasscodeLibrary>;
3030

31-
export type SendPasscodeContextPayload = Pick<SendMessagePayload, 'locale'> &
31+
export type SendPasscodeContextPayload = Pick<SendMessagePayload, 'locale' | 'uiLocales'> &
3232
VerificationCodeContextInfo;
3333

3434
export const createPasscodeLibrary = (queries: Queries, connectorLibrary: ConnectorLibrary) => {

packages/core/src/routes-me/verification-code.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { RouterInitArgs } from '#src/routes/types.js';
77

88
import RequestError from '../errors/RequestError/index.js';
99
import assertThat from '../utils/assert-that.js';
10+
import { getLogtoCookie } from '../utils/cookie.js';
1011

1112
import type { AuthedMeRouter } from './types.js';
1213

@@ -31,7 +32,11 @@ export default function verificationCodeRoutes<T extends AuthedMeRouter>(
3132
}),
3233
async (ctx, next) => {
3334
const code = await createPasscode(undefined, codeType, ctx.guard.body);
34-
await sendPasscode(code, { locale: ctx.locale });
35+
const { uiLocales } = getLogtoCookie(ctx);
36+
await sendPasscode(code, {
37+
locale: ctx.locale,
38+
uiLocales,
39+
});
3540

3641
ctx.status = 204;
3742

packages/core/src/routes/experience/verification-routes/verification-code-helpers.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
import {
22
InteractionEvent,
3-
logtoCookieKey,
4-
logtoUiCookieGuard,
53
SentinelActivityAction,
64
SignInIdentifier,
75
type VerificationCodeIdentifier,
86
VerificationType,
97
type Sentinel,
108
} from '@logto/schemas';
119
import { Action } from '@logto/schemas/lib/types/log/interaction.js';
12-
import { trySafe } from '@silverhand/essentials';
1310

1411
import RequestError from '#src/errors/RequestError/index.js';
1512
import { type PasscodeLibrary } from '#src/libraries/passcode.js';
1613
import { type LogContext } from '#src/middleware/koa-audit-log.js';
1714
import type Libraries from '#src/tenants/Libraries.js';
1815
import type Queries from '#src/tenants/Queries.js';
16+
import { getLogtoCookie } from '#src/utils/cookie.js';
1917

2018
import type ExperienceInteraction from '../classes/experience-interaction.js';
2119
import { withSentinel } from '../classes/libraries/sentinel-guard.js';
@@ -55,10 +53,7 @@ const buildVerificationCodeTemplateContext = async (
5553
return {};
5654
}
5755

58-
// Safely get the orgId and appId context from cookie
59-
const { appId: applicationId, organizationId } =
60-
trySafe(() => logtoUiCookieGuard.parse(JSON.parse(ctx.cookies.get(logtoCookieKey) ?? '{}'))) ??
61-
{};
56+
const { appId: applicationId, organizationId } = getLogtoCookie(ctx);
6257

6358
return passcodeLibrary.buildVerificationCodeContext({
6459
applicationId,
@@ -116,6 +111,7 @@ export const sendCode = async ({
116111
// Send verification code
117112
await codeVerification.sendVerificationCode({
118113
locale: ctx.locale,
114+
uiLocales: getLogtoCookie(ctx).uiLocales,
119115
...templateContext,
120116
});
121117

packages/core/src/routes/interaction/additional.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import {
22
InteractionEvent,
3-
logtoCookieKey,
4-
logtoUiCookieGuard,
53
MfaFactor,
64
type RequestVerificationCodePayload,
75
requestVerificationCodePayloadGuard,
86
webAuthnAuthenticationOptionsGuard,
97
webAuthnRegistrationOptionsGuard,
108
} from '@logto/schemas';
119
import { getUserDisplayName } from '@logto/shared';
12-
import { trySafe } from '@silverhand/essentials';
1310
import { type Context } from 'koa';
1411
import type Router from 'koa-router';
1512
import { type IRouterParamContext } from 'koa-router';
@@ -25,6 +22,7 @@ import koaGuard from '#src/middleware/koa-guard.js';
2522
import { type WithI18nContext } from '#src/middleware/koa-i18next.js';
2623
import type TenantContext from '#src/tenants/TenantContext.js';
2724
import assertThat from '#src/utils/assert-that.js';
25+
import { getLogtoCookie } from '#src/utils/cookie.js';
2826

2927
import { parseUserProfile } from './actions/helpers.js';
3028
import { interactionPrefix, verificationPath } from './const.js';
@@ -56,9 +54,7 @@ const buildVerificationCodeTemplateContext = async (
5654
}
5755

5856
// Safely get the orgId and appId context from cookie
59-
const { appId: applicationId, organizationId } =
60-
trySafe(() => logtoUiCookieGuard.parse(JSON.parse(ctx.cookies.get(logtoCookieKey) ?? '{}'))) ??
61-
{};
57+
const { appId: applicationId, organizationId } = getLogtoCookie(ctx);
6258

6359
return passcodeLibrary.buildVerificationCodeContext({
6460
applicationId,
@@ -129,7 +125,13 @@ export default function additionalRoutes<T extends IRouterParamContext>(
129125
const messageContext = await buildVerificationCodeTemplateContext(passcodes, ctx, guard.body);
130126

131127
await sendVerificationCodeToIdentifier(
132-
{ event, ...guard.body, locale: ctx.locale, messageContext },
128+
{
129+
event,
130+
...guard.body,
131+
locale: ctx.locale,
132+
uiLocales: getLogtoCookie(ctx).uiLocales,
133+
messageContext,
134+
},
133135
interactionDetails.jti,
134136
createLog,
135137
passcodes

packages/core/src/routes/interaction/utils/verification-code-validation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const sendVerificationCodeToIdentifier = async (
2626
payload: RequestVerificationCodePayload & {
2727
event: InteractionEvent;
2828
locale?: string;
29+
uiLocales?: string;
2930
messageContext?: VerificationCodeContextInfo;
3031
},
3132
jti: string,

packages/core/src/routes/verification/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { z } from 'zod';
1414

1515
import koaGuard from '#src/middleware/koa-guard.js';
16+
import { getLogtoCookie } from '#src/utils/cookie.js';
1617

1718
import {
1819
buildVerificationRecordByIdAndType,
@@ -118,8 +119,11 @@ export default function verificationRoutes<T extends UserRouter>(
118119
? await libraries.passcodes.buildVerificationCodeContext({ user, applicationId })
119120
: undefined;
120121

122+
const { uiLocales } = getLogtoCookie(ctx);
123+
121124
await codeVerification.sendVerificationCode({
122125
locale: ctx.locale,
126+
uiLocales,
123127
...emailContextPayload,
124128
});
125129

packages/core/src/utils/cookie.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { logtoUiCookieGuard, logtoCookieKey } from '@logto/schemas';
2+
import { trySafe } from '@silverhand/essentials';
3+
import { type Context } from 'koa';
4+
5+
export const getLogtoCookie = (ctx: Context) =>
6+
trySafe(() => logtoUiCookieGuard.parse(JSON.parse(ctx.cookies.get(logtoCookieKey) ?? '{}'))) ??
7+
{};

packages/toolkit/connector-kit/src/types/passwordless.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ export type SendMessagePayload = {
6666
* @example 'en-US'
6767
*/
6868
locale?: string;
69+
/**
70+
* The `ui_locales` parameter from the authentication request, which can be used to localize the message.
71+
* This is different from `locale` as it is the original request parameter and may contain multiple language
72+
* tags sorted by user's preference.
73+
* The `locale` field, is the single language tag resolved from multiple sources, and the precedence is:
74+
* `ui_locales` > HTTP `Accept-Language` header > default fallback (en).
75+
*
76+
* @remarks
77+
* For email connectors that handle email templates at the provider side, use this field to indicate the user's preferred language.
78+
*
79+
* @example 'en-US en'
80+
*/
81+
uiLocales?: string;
6982
} & Record<string, unknown>;
7083

7184
/** The guard for {@link SendMessagePayload}. */
@@ -74,6 +87,7 @@ export const sendMessagePayloadGuard = z
7487
code: z.string().optional(),
7588
link: z.string().optional(),
7689
locale: z.string().optional(),
90+
uiLocales: z.string().optional(),
7791
})
7892
.catchall(z.unknown()) satisfies z.ZodType<SendMessagePayload>;
7993

0 commit comments

Comments
 (0)