@learncard/email-templates 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Layout.d.ts +2 -0
- package/dist/components/Layout.d.ts.map +1 -1
- package/dist/components/Layout.js +6 -1
- package/dist/components/Layout.js.map +1 -1
- package/dist/components/LinkFallback.d.ts +2 -0
- package/dist/components/LinkFallback.d.ts.map +1 -1
- package/dist/components/LinkFallback.js +5 -1
- package/dist/components/LinkFallback.js.map +1 -1
- package/dist/i18n.d.ts +52 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +77 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/render.d.ts +3 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +33 -30
- package/dist/render.js.map +1 -1
- package/dist/sms.d.ts +6 -3
- package/dist/sms.d.ts.map +1 -1
- package/dist/sms.js +43 -7
- package/dist/sms.js.map +1 -1
- package/dist/templates/account-approved.d.ts +3 -1
- package/dist/templates/account-approved.d.ts.map +1 -1
- package/dist/templates/account-approved.js +55 -5
- package/dist/templates/account-approved.js.map +1 -1
- package/dist/templates/credential-awaiting-guardian.d.ts +3 -1
- package/dist/templates/credential-awaiting-guardian.d.ts.map +1 -1
- package/dist/templates/credential-awaiting-guardian.js +54 -4
- package/dist/templates/credential-awaiting-guardian.js.map +1 -1
- package/dist/templates/email-verification.d.ts +3 -1
- package/dist/templates/email-verification.d.ts.map +1 -1
- package/dist/templates/email-verification.js +57 -4
- package/dist/templates/email-verification.js.map +1 -1
- package/dist/templates/endorsement-request.d.ts +3 -1
- package/dist/templates/endorsement-request.d.ts.map +1 -1
- package/dist/templates/endorsement-request.js +53 -5
- package/dist/templates/endorsement-request.js.map +1 -1
- package/dist/templates/guardian-approval.d.ts +3 -1
- package/dist/templates/guardian-approval.d.ts.map +1 -1
- package/dist/templates/guardian-approval.js +55 -5
- package/dist/templates/guardian-approval.js.map +1 -1
- package/dist/templates/guardian-approved-claim.d.ts +3 -1
- package/dist/templates/guardian-approved-claim.d.ts.map +1 -1
- package/dist/templates/guardian-approved-claim.js +50 -4
- package/dist/templates/guardian-approved-claim.js.map +1 -1
- package/dist/templates/guardian-credential-approval.d.ts +3 -1
- package/dist/templates/guardian-credential-approval.d.ts.map +1 -1
- package/dist/templates/guardian-credential-approval.js +50 -4
- package/dist/templates/guardian-credential-approval.js.map +1 -1
- package/dist/templates/guardian-email-otp.d.ts +3 -1
- package/dist/templates/guardian-email-otp.d.ts.map +1 -1
- package/dist/templates/guardian-email-otp.js +55 -5
- package/dist/templates/guardian-email-otp.js.map +1 -1
- package/dist/templates/guardian-rejected-credential.d.ts +3 -1
- package/dist/templates/guardian-rejected-credential.d.ts.map +1 -1
- package/dist/templates/guardian-rejected-credential.js +45 -3
- package/dist/templates/guardian-rejected-credential.js.map +1 -1
- package/dist/templates/inbox-claim.d.ts +3 -1
- package/dist/templates/inbox-claim.d.ts.map +1 -1
- package/dist/templates/inbox-claim.js +63 -11
- package/dist/templates/inbox-claim.js.map +1 -1
- package/dist/templates/recovery-key.d.ts +3 -1
- package/dist/templates/recovery-key.d.ts.map +1 -1
- package/dist/templates/recovery-key.js +59 -3
- package/dist/templates/recovery-key.js.map +1 -1
- package/dist/templates/verification-code.d.ts +3 -1
- package/dist/templates/verification-code.d.ts.map +1 -1
- package/dist/templates/verification-code.js +153 -17
- package/dist/templates/verification-code.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/i18n.test.ts +329 -0
- package/src/components/Layout.tsx +69 -60
- package/src/components/LinkFallback.tsx +19 -11
- package/src/i18n.ts +110 -0
- package/src/index.ts +15 -2
- package/src/render.ts +47 -28
- package/src/sms.ts +61 -6
- package/src/templates/account-approved.tsx +82 -29
- package/src/templates/credential-awaiting-guardian.tsx +113 -19
- package/src/templates/email-verification.tsx +87 -16
- package/src/templates/endorsement-request.tsx +142 -17
- package/src/templates/guardian-approval.tsx +106 -16
- package/src/templates/guardian-approved-claim.tsx +137 -17
- package/src/templates/guardian-credential-approval.tsx +112 -17
- package/src/templates/guardian-email-otp.tsx +103 -25
- package/src/templates/guardian-rejected-credential.tsx +137 -15
- package/src/templates/inbox-claim.tsx +180 -29
- package/src/templates/recovery-key.tsx +126 -30
- package/src/templates/verification-code.tsx +204 -38
package/src/render.ts
CHANGED
|
@@ -239,10 +239,12 @@ export async function renderEmail<T extends TemplateId>(
|
|
|
239
239
|
templateId: T,
|
|
240
240
|
branding: Partial<TenantBranding> | TenantBranding,
|
|
241
241
|
data: TemplateDataMap[T],
|
|
242
|
+
/** Recipient locale (BCP-47). Defaults to English. */
|
|
243
|
+
locale?: string
|
|
242
244
|
): Promise<RenderedEmail> {
|
|
243
245
|
const resolved = resolveBranding(branding);
|
|
244
246
|
|
|
245
|
-
const { element, subject } = buildElement(templateId, resolved, data);
|
|
247
|
+
const { element, subject } = buildElement(templateId, resolved, data, locale);
|
|
246
248
|
|
|
247
249
|
const html = await render(element);
|
|
248
250
|
const text = await render(element, { plainText: true });
|
|
@@ -258,6 +260,7 @@ function buildVerificationElement(
|
|
|
258
260
|
branding: TenantBranding,
|
|
259
261
|
data: VerificationCodeData,
|
|
260
262
|
variant: VerificationCodeVariant,
|
|
263
|
+
locale?: string
|
|
261
264
|
): { element: React.ReactElement; subject: string } {
|
|
262
265
|
return {
|
|
263
266
|
element: React.createElement(VerificationCode, {
|
|
@@ -265,8 +268,9 @@ function buildVerificationElement(
|
|
|
265
268
|
verificationCode: data.verificationCode,
|
|
266
269
|
verificationEmail: data.verificationEmail,
|
|
267
270
|
variant,
|
|
271
|
+
locale,
|
|
268
272
|
}),
|
|
269
|
-
subject: getVerificationCodeSubject(branding, variant),
|
|
273
|
+
subject: getVerificationCodeSubject(branding, variant, locale),
|
|
270
274
|
};
|
|
271
275
|
}
|
|
272
276
|
|
|
@@ -274,48 +278,63 @@ function buildElement(
|
|
|
274
278
|
templateId: TemplateId,
|
|
275
279
|
branding: TenantBranding,
|
|
276
280
|
data: TemplateDataMap[TemplateId],
|
|
281
|
+
locale?: string
|
|
277
282
|
): { element: React.ReactElement; subject: string } {
|
|
278
|
-
|
|
279
283
|
switch (templateId) {
|
|
280
284
|
case 'embed-email-verification':
|
|
281
|
-
return buildVerificationElement(
|
|
285
|
+
return buildVerificationElement(
|
|
286
|
+
branding,
|
|
287
|
+
data as VerificationCodeData,
|
|
288
|
+
'embed-verification',
|
|
289
|
+
locale
|
|
290
|
+
);
|
|
282
291
|
|
|
283
292
|
case 'contact-method-verification': {
|
|
284
293
|
const d = data as EmailVerificationData;
|
|
285
|
-
const props: EmailVerificationProps = { branding, ...d };
|
|
294
|
+
const props: EmailVerificationProps = { branding, ...d, locale };
|
|
286
295
|
|
|
287
296
|
return {
|
|
288
297
|
element: React.createElement(EmailVerification, props),
|
|
289
|
-
subject: getEmailVerificationSubject(branding),
|
|
298
|
+
subject: getEmailVerificationSubject(branding, locale),
|
|
290
299
|
};
|
|
291
300
|
}
|
|
292
301
|
|
|
293
302
|
case 'login-verification-code':
|
|
294
303
|
case 'contact-method-verification-1':
|
|
295
|
-
return buildVerificationElement(
|
|
304
|
+
return buildVerificationElement(
|
|
305
|
+
branding,
|
|
306
|
+
data as VerificationCodeData,
|
|
307
|
+
'login',
|
|
308
|
+
locale
|
|
309
|
+
);
|
|
296
310
|
|
|
297
311
|
case 'recovery-email-code':
|
|
298
312
|
case 'recovery-email-verification':
|
|
299
|
-
return buildVerificationElement(
|
|
313
|
+
return buildVerificationElement(
|
|
314
|
+
branding,
|
|
315
|
+
data as VerificationCodeData,
|
|
316
|
+
'recovery-email',
|
|
317
|
+
locale
|
|
318
|
+
);
|
|
300
319
|
|
|
301
320
|
case 'inbox-claim':
|
|
302
321
|
case 'universal-inbox-claim': {
|
|
303
322
|
const d = data as InboxClaimData;
|
|
304
|
-
const props: InboxClaimProps = { branding, ...d };
|
|
323
|
+
const props: InboxClaimProps = { branding, ...d, locale };
|
|
305
324
|
|
|
306
325
|
return {
|
|
307
326
|
element: React.createElement(InboxClaim, props),
|
|
308
|
-
subject: getInboxClaimSubject(branding, props),
|
|
327
|
+
subject: getInboxClaimSubject(branding, props, locale),
|
|
309
328
|
};
|
|
310
329
|
}
|
|
311
330
|
|
|
312
331
|
case 'guardian-approval': {
|
|
313
332
|
const d = data as GuardianApprovalData;
|
|
314
|
-
const props: GuardianApprovalProps = { branding, ...d };
|
|
333
|
+
const props: GuardianApprovalProps = { branding, ...d, locale };
|
|
315
334
|
|
|
316
335
|
return {
|
|
317
336
|
element: React.createElement(GuardianApproval, props),
|
|
318
|
-
subject: getGuardianApprovalSubject(branding, props),
|
|
337
|
+
subject: getGuardianApprovalSubject(branding, props, locale),
|
|
319
338
|
};
|
|
320
339
|
}
|
|
321
340
|
|
|
@@ -324,80 +343,80 @@ function buildElement(
|
|
|
324
343
|
const d = data as AccountApprovedData;
|
|
325
344
|
|
|
326
345
|
return {
|
|
327
|
-
element: React.createElement(AccountApproved, { branding, ...d }),
|
|
328
|
-
subject: getAccountApprovedSubject(branding),
|
|
346
|
+
element: React.createElement(AccountApproved, { branding, ...d, locale }),
|
|
347
|
+
subject: getAccountApprovedSubject(branding, locale),
|
|
329
348
|
};
|
|
330
349
|
}
|
|
331
350
|
|
|
332
351
|
case 'recovery-key':
|
|
333
352
|
case 'recovery-key-backup': {
|
|
334
353
|
const d = data as RecoveryKeyData;
|
|
335
|
-
const props: RecoveryKeyProps = { branding, ...d };
|
|
354
|
+
const props: RecoveryKeyProps = { branding, ...d, locale };
|
|
336
355
|
|
|
337
356
|
return {
|
|
338
357
|
element: React.createElement(RecoveryKey, props),
|
|
339
|
-
subject: getRecoveryKeySubject(branding),
|
|
358
|
+
subject: getRecoveryKeySubject(branding, locale),
|
|
340
359
|
};
|
|
341
360
|
}
|
|
342
361
|
|
|
343
362
|
case 'endorsement-request':
|
|
344
363
|
case 'universal-inbox-claim-1': {
|
|
345
364
|
const d = data as EndorsementRequestData;
|
|
346
|
-
const props: EndorsementRequestProps = { branding, ...d };
|
|
365
|
+
const props: EndorsementRequestProps = { branding, ...d, locale };
|
|
347
366
|
|
|
348
367
|
return {
|
|
349
368
|
element: React.createElement(EndorsementRequest, props),
|
|
350
|
-
subject: getEndorsementRequestSubject(branding, props),
|
|
369
|
+
subject: getEndorsementRequestSubject(branding, props, locale),
|
|
351
370
|
};
|
|
352
371
|
}
|
|
353
372
|
|
|
354
373
|
case 'credential-awaiting-guardian': {
|
|
355
374
|
const d = data as CredentialAwaitingGuardianData;
|
|
356
|
-
const props: CredentialAwaitingGuardianProps = { branding, ...d };
|
|
375
|
+
const props: CredentialAwaitingGuardianProps = { branding, ...d, locale };
|
|
357
376
|
|
|
358
377
|
return {
|
|
359
378
|
element: React.createElement(CredentialAwaitingGuardian, props),
|
|
360
|
-
subject: getCredentialAwaitingGuardianSubject(branding),
|
|
379
|
+
subject: getCredentialAwaitingGuardianSubject(branding, locale),
|
|
361
380
|
};
|
|
362
381
|
}
|
|
363
382
|
|
|
364
383
|
case 'guardian-approved-claim': {
|
|
365
384
|
const d = data as GuardianApprovedClaimData;
|
|
366
|
-
const props: GuardianApprovedClaimProps = { branding, ...d };
|
|
385
|
+
const props: GuardianApprovedClaimProps = { branding, ...d, locale };
|
|
367
386
|
|
|
368
387
|
return {
|
|
369
388
|
element: React.createElement(GuardianApprovedClaim, props),
|
|
370
|
-
subject: getGuardianApprovedClaimSubject(branding),
|
|
389
|
+
subject: getGuardianApprovedClaimSubject(branding, locale),
|
|
371
390
|
};
|
|
372
391
|
}
|
|
373
392
|
|
|
374
393
|
case 'guardian-credential-approval': {
|
|
375
394
|
const d = data as GuardianCredentialApprovalData;
|
|
376
|
-
const props: GuardianCredentialApprovalProps = { branding, ...d };
|
|
395
|
+
const props: GuardianCredentialApprovalProps = { branding, ...d, locale };
|
|
377
396
|
|
|
378
397
|
return {
|
|
379
398
|
element: React.createElement(GuardianCredentialApproval, props),
|
|
380
|
-
subject: getGuardianCredentialApprovalSubject(branding),
|
|
399
|
+
subject: getGuardianCredentialApprovalSubject(branding, locale),
|
|
381
400
|
};
|
|
382
401
|
}
|
|
383
402
|
|
|
384
403
|
case 'guardian-email-otp': {
|
|
385
404
|
const d = data as GuardianEmailOtpData;
|
|
386
|
-
const props: GuardianEmailOtpProps = { branding, ...d };
|
|
405
|
+
const props: GuardianEmailOtpProps = { branding, ...d, locale };
|
|
387
406
|
|
|
388
407
|
return {
|
|
389
408
|
element: React.createElement(GuardianEmailOtp, props),
|
|
390
|
-
subject: getGuardianEmailOtpSubject(branding),
|
|
409
|
+
subject: getGuardianEmailOtpSubject(branding, locale),
|
|
391
410
|
};
|
|
392
411
|
}
|
|
393
412
|
|
|
394
413
|
case 'guardian-rejected-credential': {
|
|
395
414
|
const d = data as GuardianRejectedCredentialData;
|
|
396
|
-
const props: GuardianRejectedCredentialProps = { branding, ...d };
|
|
415
|
+
const props: GuardianRejectedCredentialProps = { branding, ...d, locale };
|
|
397
416
|
|
|
398
417
|
return {
|
|
399
418
|
element: React.createElement(GuardianRejectedCredential, props),
|
|
400
|
-
subject: getGuardianRejectedCredentialSubject(branding),
|
|
419
|
+
subject: getGuardianRejectedCredentialSubject(branding, locale),
|
|
401
420
|
};
|
|
402
421
|
}
|
|
403
422
|
|
package/src/sms.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SMS template rendering — tenant-aware replacement for sms.helpers.ts.
|
|
3
3
|
*
|
|
4
|
-
* Replaces the hardcoded "LearnCard" in SMS text with the tenant's brand name
|
|
4
|
+
* Replaces the hardcoded "LearnCard" in SMS text with the tenant's brand name
|
|
5
|
+
* and renders localized SMS bodies (LC-1902, workstream C1).
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import type { TenantBranding } from './branding';
|
|
8
9
|
import { resolveBranding } from './branding';
|
|
10
|
+
import type { NotificationLocale } from './i18n';
|
|
11
|
+
import { resolveCatalogLocale } from './i18n';
|
|
9
12
|
|
|
10
13
|
// ---------------------------------------------------------------------------
|
|
11
14
|
// SMS template data shapes
|
|
@@ -29,12 +32,59 @@ export interface SmsTemplateDataMap {
|
|
|
29
32
|
'contact-method-verification': SmsVerificationData;
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Per-locale SMS copy. EN values byte-match the originals in sms.helpers.ts.
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
const INBOX_CLAIM: Record<
|
|
40
|
+
NotificationLocale,
|
|
41
|
+
{
|
|
42
|
+
greetingName: (name: string) => string;
|
|
43
|
+
body: (credType: string, credName: string, issuerName: string | undefined) => string;
|
|
44
|
+
cta: string;
|
|
45
|
+
}
|
|
46
|
+
> = {
|
|
47
|
+
en: {
|
|
48
|
+
greetingName: name => `Hello, ${name}! `,
|
|
49
|
+
body: (credType, credName, issuerName) =>
|
|
50
|
+
`You have a new ${credType}, "${credName}"${issuerName ? ` from ${issuerName}.` : '.'}`,
|
|
51
|
+
cta: 'Claim it here:',
|
|
52
|
+
},
|
|
53
|
+
es: {
|
|
54
|
+
greetingName: name => `¡Hola, ${name}! `,
|
|
55
|
+
body: (credType, credName, issuerName) =>
|
|
56
|
+
`Tienes un nuevo ${credType}, "${credName}"${issuerName ? ` de ${issuerName}.` : '.'}`,
|
|
57
|
+
cta: 'Reclámalo aquí:',
|
|
58
|
+
},
|
|
59
|
+
fr: {
|
|
60
|
+
greetingName: name => `Bonjour ${name} ! `,
|
|
61
|
+
body: (credType, credName, issuerName) =>
|
|
62
|
+
`Vous avez un nouveau ${credType}, « ${credName} »${
|
|
63
|
+
issuerName ? ` de ${issuerName}.` : '.'
|
|
64
|
+
}`,
|
|
65
|
+
cta: 'Réclamez-le ici :',
|
|
66
|
+
},
|
|
67
|
+
ar: {
|
|
68
|
+
greetingName: name => `مرحبًا ${name}! `,
|
|
69
|
+
body: (credType, credName, issuerName) =>
|
|
70
|
+
`لديك ${credType} جديد، "${credName}"${issuerName ? ` من ${issuerName}.` : '.'}`,
|
|
71
|
+
cta: 'طالب به هنا:',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const VERIFICATION: Record<NotificationLocale, (brandName: string) => string> = {
|
|
76
|
+
en: b => `Your ${b} verification code is:`,
|
|
77
|
+
es: b => `Tu código de verificación de ${b} es:`,
|
|
78
|
+
fr: b => `Votre code de vérification ${b} est :`,
|
|
79
|
+
ar: b => `رمز التحقق الخاص بك في ${b} هو:`,
|
|
80
|
+
};
|
|
81
|
+
|
|
32
82
|
// ---------------------------------------------------------------------------
|
|
33
83
|
// renderSms()
|
|
34
84
|
// ---------------------------------------------------------------------------
|
|
35
85
|
|
|
36
86
|
/**
|
|
37
|
-
* Render a branded SMS body for the given template.
|
|
87
|
+
* Render a branded, localized SMS body for the given template.
|
|
38
88
|
*
|
|
39
89
|
* @returns The SMS body string, or `undefined` if the templateId is unknown.
|
|
40
90
|
*/
|
|
@@ -42,24 +92,29 @@ export function renderSms<T extends SmsTemplateId>(
|
|
|
42
92
|
templateId: T,
|
|
43
93
|
branding: Partial<TenantBranding> | TenantBranding,
|
|
44
94
|
data: SmsTemplateDataMap[T],
|
|
95
|
+
/** Recipient locale (BCP-47). Defaults to English. */
|
|
96
|
+
locale?: string
|
|
45
97
|
): string | undefined {
|
|
46
98
|
const resolved = resolveBranding(branding);
|
|
99
|
+
const loc = resolveCatalogLocale(locale);
|
|
47
100
|
|
|
48
101
|
switch (templateId) {
|
|
49
102
|
case 'inbox-claim': {
|
|
50
103
|
const d = data as SmsInboxClaimData;
|
|
51
|
-
const
|
|
104
|
+
const s = INBOX_CLAIM[loc];
|
|
105
|
+
const greeting = d.recipient?.name ? s.greetingName(d.recipient.name) : '';
|
|
52
106
|
const credType = d.credential?.type ?? 'credential';
|
|
53
107
|
const credName = d.credential?.name ?? 'Unnamed Credential';
|
|
54
|
-
const issuerPart = d.issuer?.name ? ` from ${d.issuer.name}.` : '.';
|
|
55
108
|
|
|
56
|
-
return `${greeting}
|
|
109
|
+
return `${greeting}${s.body(credType, credName, d.issuer?.name)} ${s.cta} ${
|
|
110
|
+
d.claimUrl
|
|
111
|
+
}`;
|
|
57
112
|
}
|
|
58
113
|
|
|
59
114
|
case 'contact-method-verification': {
|
|
60
115
|
const d = data as SmsVerificationData;
|
|
61
116
|
|
|
62
|
-
return
|
|
117
|
+
return `${VERIFICATION[loc](resolved.brandName)} ${d.verificationToken}`;
|
|
63
118
|
}
|
|
64
119
|
|
|
65
120
|
default:
|
|
@@ -11,56 +11,114 @@ import type { TenantBranding } from '../branding';
|
|
|
11
11
|
import { DEFAULT_BRANDING } from '../branding';
|
|
12
12
|
import { Layout } from '../components/Layout';
|
|
13
13
|
import { EmailButton } from '../components/EmailButton';
|
|
14
|
+
import type { NotificationLocale } from '../i18n';
|
|
15
|
+
import { resolveCatalogLocale, interpolate, SHARED } from '../i18n';
|
|
14
16
|
|
|
15
17
|
export interface AccountApprovedProps {
|
|
16
18
|
branding: TenantBranding;
|
|
17
19
|
user?: {
|
|
18
20
|
displayName?: string;
|
|
19
21
|
};
|
|
22
|
+
/** Recipient locale (BCP-47). Defaults to English. */
|
|
23
|
+
locale?: string;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
const STRINGS: Record<
|
|
27
|
+
NotificationLocale,
|
|
28
|
+
{
|
|
29
|
+
preview: (brandName: string) => string;
|
|
30
|
+
heading: (brandName: string) => string;
|
|
31
|
+
greeting: (name: string) => string;
|
|
32
|
+
body1: (brandName: string) => string;
|
|
33
|
+
body2: string;
|
|
34
|
+
ctaInstruction: string;
|
|
35
|
+
button: (brandName: string) => string;
|
|
36
|
+
thankYou: string;
|
|
37
|
+
subject: (brandName: string) => string;
|
|
38
|
+
}
|
|
39
|
+
> = {
|
|
40
|
+
en: {
|
|
41
|
+
preview: b => `Your ${b} account has been approved!`,
|
|
42
|
+
heading: b => `Your ${b} account has been approved!`,
|
|
43
|
+
greeting: name => `Hello ${name},`,
|
|
44
|
+
body1: b => `Your ${b} account has been approved by your guardian/parent.`,
|
|
45
|
+
body2: '{brandName} is a private, digital passport for learning and work. It lets users securely collect and share their verified skills and achievements online.',
|
|
46
|
+
ctaInstruction: 'Please click the button below to login to your account.',
|
|
47
|
+
button: b => `Login to ${b} \u2192`,
|
|
48
|
+
thankYou: 'Thank you,',
|
|
49
|
+
subject: b => `Your ${b} account has been approved`,
|
|
50
|
+
},
|
|
51
|
+
es: {
|
|
52
|
+
preview: b => `¡Tu cuenta de ${b} ha sido aprobada!`,
|
|
53
|
+
heading: b => `¡Tu cuenta de ${b} ha sido aprobada!`,
|
|
54
|
+
greeting: name => `Hola ${name},`,
|
|
55
|
+
body1: b => `Tu cuenta de ${b} ha sido aprobada por tu tutor o progenitor.`,
|
|
56
|
+
body2: '{brandName} es un pasaporte digital y privado para el aprendizaje y el trabajo. Permite a los usuarios recopilar y compartir de forma segura sus habilidades y logros verificados en línea.',
|
|
57
|
+
ctaInstruction: 'Haz clic en el botón siguiente para iniciar sesión en tu cuenta.',
|
|
58
|
+
button: b => `Iniciar sesión en ${b} \u2192`,
|
|
59
|
+
thankYou: 'Gracias,',
|
|
60
|
+
subject: b => `Tu cuenta de ${b} ha sido aprobada`,
|
|
61
|
+
},
|
|
62
|
+
fr: {
|
|
63
|
+
preview: b => `Votre compte ${b} a été approuvé !`,
|
|
64
|
+
heading: b => `Votre compte ${b} a été approuvé !`,
|
|
65
|
+
greeting: name => `Bonjour ${name},`,
|
|
66
|
+
body1: b => `Votre compte ${b} a été approuvé par votre tuteur ou parent.`,
|
|
67
|
+
body2: '{brandName} est un passeport numérique privé pour l\u2019apprentissage et le travail. Il permet aux utilisateurs de collecter et de partager en toute sécurité leurs compétences et réalisations vérifiées en ligne.',
|
|
68
|
+
ctaInstruction:
|
|
69
|
+
'Veuillez cliquer sur le bouton ci-dessous pour vous connecter à votre compte.',
|
|
70
|
+
button: b => `Se connecter à ${b} \u2192`,
|
|
71
|
+
thankYou: 'Merci,',
|
|
72
|
+
subject: b => `Votre compte ${b} a été approuvé`,
|
|
73
|
+
},
|
|
74
|
+
ar: {
|
|
75
|
+
preview: b => `تمت الموافقة على حسابك في ${b}!`,
|
|
76
|
+
heading: b => `تمت الموافقة على حسابك في ${b}!`,
|
|
77
|
+
greeting: name => `مرحبًا ${name}،`,
|
|
78
|
+
body1: b => `تمت الموافقة على حسابك في ${b} من قبل ولي أمرك.`,
|
|
79
|
+
body2: '{brandName} هو جواز سفر رقمي خاص للتعلّم والعمل. يتيح للمستخدمين جمع ومشاركة مهاراتهم وإنجازاتهم الموثّقة بأمان عبر الإنترنت.',
|
|
80
|
+
ctaInstruction: 'يرجى النقر على الزر أدناه لتسجيل الدخول إلى حسابك.',
|
|
81
|
+
button: b => `تسجيل الدخول إلى ${b} ←`,
|
|
82
|
+
thankYou: 'شكرًا،',
|
|
83
|
+
subject: b => `تمت الموافقة على حسابك في ${b}`,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const AccountApproved: React.FC<AccountApprovedProps> = ({ branding, user, locale }) => {
|
|
88
|
+
const s = STRINGS[resolveCatalogLocale(locale)];
|
|
26
89
|
const name = user?.displayName ?? 'there';
|
|
27
90
|
|
|
28
91
|
return (
|
|
29
|
-
<Layout branding={branding}
|
|
30
|
-
<Text style={heading}>
|
|
92
|
+
<Layout branding={branding} locale={locale} preview={s.preview(branding.brandName)}>
|
|
93
|
+
<Text style={heading}>{s.heading(branding.brandName)}</Text>
|
|
31
94
|
|
|
32
|
-
<Text style={paragraph}>
|
|
33
|
-
Hello {name},
|
|
34
|
-
</Text>
|
|
95
|
+
<Text style={paragraph}>{s.greeting(name)}</Text>
|
|
35
96
|
|
|
36
|
-
<Text style={paragraph}>
|
|
37
|
-
Your {branding.brandName} account has been approved by your guardian/parent.
|
|
38
|
-
</Text>
|
|
97
|
+
<Text style={paragraph}>{s.body1(branding.brandName)}</Text>
|
|
39
98
|
|
|
40
|
-
<Text style={paragraph}>
|
|
41
|
-
{branding.brandName} is a private, digital passport for learning and work.
|
|
42
|
-
It lets users securely collect and share their verified skills and achievements online.
|
|
43
|
-
</Text>
|
|
99
|
+
<Text style={paragraph}>{interpolate(s.body2, { brandName: branding.brandName })}</Text>
|
|
44
100
|
|
|
45
|
-
<Text style={paragraph}>
|
|
46
|
-
Please click the button below to login to your account.
|
|
47
|
-
</Text>
|
|
101
|
+
<Text style={paragraph}>{s.ctaInstruction}</Text>
|
|
48
102
|
|
|
49
103
|
<Section style={buttonWrapper}>
|
|
50
104
|
<EmailButton href={branding.appUrl} branding={branding}>
|
|
51
|
-
|
|
105
|
+
{s.button(branding.brandName)}
|
|
52
106
|
</EmailButton>
|
|
53
107
|
</Section>
|
|
54
108
|
|
|
55
109
|
<Text style={signOff}>
|
|
56
|
-
|
|
110
|
+
{s.thankYou}
|
|
111
|
+
<br />
|
|
112
|
+
{interpolate(SHARED[resolveCatalogLocale(locale)].teamSignature, {
|
|
113
|
+
brandName: branding.brandName,
|
|
114
|
+
})}
|
|
57
115
|
</Text>
|
|
58
116
|
</Layout>
|
|
59
117
|
);
|
|
60
118
|
};
|
|
61
119
|
|
|
62
|
-
export const getAccountApprovedSubject = (branding: TenantBranding): string =>
|
|
63
|
-
|
|
120
|
+
export const getAccountApprovedSubject = (branding: TenantBranding, locale?: string): string =>
|
|
121
|
+
STRINGS[resolveCatalogLocale(locale)].subject(branding.brandName);
|
|
64
122
|
|
|
65
123
|
// ---------------------------------------------------------------------------
|
|
66
124
|
// Styles
|
|
@@ -96,10 +154,5 @@ const signOff: React.CSSProperties = {
|
|
|
96
154
|
// ---------------------------------------------------------------------------
|
|
97
155
|
|
|
98
156
|
export default function Preview() {
|
|
99
|
-
return
|
|
100
|
-
<AccountApproved
|
|
101
|
-
branding={DEFAULT_BRANDING}
|
|
102
|
-
user={{ displayName: 'Jane Doe' }}
|
|
103
|
-
/>
|
|
104
|
-
);
|
|
157
|
+
return <AccountApproved branding={DEFAULT_BRANDING} user={{ displayName: 'Jane Doe' }} />;
|
|
105
158
|
}
|
|
@@ -12,6 +12,8 @@ import type { TenantBranding } from '../branding';
|
|
|
12
12
|
import { DEFAULT_BRANDING } from '../branding';
|
|
13
13
|
import { Layout } from '../components/Layout';
|
|
14
14
|
import { IssuerLogo } from '../components/IssuerLogo';
|
|
15
|
+
import type { NotificationLocale } from '../i18n';
|
|
16
|
+
import { resolveCatalogLocale, interpolate, SHARED } from '../i18n';
|
|
15
17
|
|
|
16
18
|
export interface CredentialAwaitingGuardianProps {
|
|
17
19
|
branding: TenantBranding;
|
|
@@ -25,45 +27,136 @@ export interface CredentialAwaitingGuardianProps {
|
|
|
25
27
|
recipient?: {
|
|
26
28
|
email?: string;
|
|
27
29
|
};
|
|
30
|
+
/** Recipient locale (BCP-47). Defaults to English. */
|
|
31
|
+
locale?: string;
|
|
28
32
|
}
|
|
29
33
|
|
|
34
|
+
const STRINGS: Record<
|
|
35
|
+
NotificationLocale,
|
|
36
|
+
{
|
|
37
|
+
preview: (credentialName: string) => string;
|
|
38
|
+
heading: string;
|
|
39
|
+
greeting: string;
|
|
40
|
+
body1: (issuerName: string, credentialName: string) => React.ReactNode;
|
|
41
|
+
body2: (brandName: string) => string;
|
|
42
|
+
statusBadge: string;
|
|
43
|
+
muted: string;
|
|
44
|
+
sincerely: string;
|
|
45
|
+
subject: string;
|
|
46
|
+
}
|
|
47
|
+
> = {
|
|
48
|
+
en: {
|
|
49
|
+
preview: c => `Your credential "${c}" is pending guardian approval`,
|
|
50
|
+
heading: 'Pending guardian approval',
|
|
51
|
+
greeting: 'Hi there,',
|
|
52
|
+
body1: (issuerName, credentialName) => (
|
|
53
|
+
<>
|
|
54
|
+
<strong>{issuerName}</strong> has sent you the credential{' '}
|
|
55
|
+
<strong>“{credentialName}”</strong>.
|
|
56
|
+
</>
|
|
57
|
+
),
|
|
58
|
+
body2: b =>
|
|
59
|
+
`Before you can claim it, your guardian needs to review and approve it first. We\u2019ve already sent them a notification \u2014 once they approve, you\u2019ll receive a follow-up email and the credential will appear in your ${b} account.`,
|
|
60
|
+
statusBadge: 'Awaiting Guardian Approval',
|
|
61
|
+
muted: 'No action is needed from you right now.',
|
|
62
|
+
sincerely: 'Sincerely,',
|
|
63
|
+
subject: 'Your Credential Is Pending Guardian Approval',
|
|
64
|
+
},
|
|
65
|
+
es: {
|
|
66
|
+
preview: c => `Tu credencial "${c}" está pendiente de aprobación del tutor`,
|
|
67
|
+
heading: 'Pendiente de aprobación del tutor',
|
|
68
|
+
greeting: 'Hola,',
|
|
69
|
+
body1: (issuerName, credentialName) => (
|
|
70
|
+
<>
|
|
71
|
+
<strong>{issuerName}</strong> te ha enviado la credencial{' '}
|
|
72
|
+
<strong>“{credentialName}”</strong>.
|
|
73
|
+
</>
|
|
74
|
+
),
|
|
75
|
+
body2: b =>
|
|
76
|
+
`Antes de poder reclamarla, tu tutor debe revisarla y aprobarla primero. Ya le hemos enviado una notificación \u2014 cuando la apruebe, recibirás un correo de seguimiento y la credencial aparecerá en tu cuenta de ${b}.`,
|
|
77
|
+
statusBadge: 'Pendiente de aprobación del tutor',
|
|
78
|
+
muted: 'No necesitas realizar ninguna acción en este momento.',
|
|
79
|
+
sincerely: 'Atentamente,',
|
|
80
|
+
subject: 'Tu credencial está pendiente de aprobación del tutor',
|
|
81
|
+
},
|
|
82
|
+
fr: {
|
|
83
|
+
preview: c => `Votre titre « ${c} » est en attente d\u2019approbation du tuteur`,
|
|
84
|
+
heading: 'En attente d\u2019approbation du tuteur',
|
|
85
|
+
greeting: 'Bonjour,',
|
|
86
|
+
body1: (issuerName, credentialName) => (
|
|
87
|
+
<>
|
|
88
|
+
<strong>{issuerName}</strong> vous a envoyé le titre{' '}
|
|
89
|
+
<strong>« {credentialName} »</strong>.
|
|
90
|
+
</>
|
|
91
|
+
),
|
|
92
|
+
body2: b =>
|
|
93
|
+
`Avant de pouvoir le réclamer, votre tuteur doit d\u2019abord le vérifier et l\u2019approuver. Nous lui avons déjà envoyé une notification \u2014 une fois qu\u2019il aura approuvé, vous recevrez un e-mail de suivi et le titre apparaîtra dans votre compte ${b}.`,
|
|
94
|
+
statusBadge: 'En attente d\u2019approbation du tuteur',
|
|
95
|
+
muted: 'Aucune action n\u2019est requise de votre part pour le moment.',
|
|
96
|
+
sincerely: 'Cordialement,',
|
|
97
|
+
subject: 'Votre titre est en attente d\u2019approbation du tuteur',
|
|
98
|
+
},
|
|
99
|
+
ar: {
|
|
100
|
+
preview: c => `شهادتك "${c}" بانتظار موافقة ولي الأمر`,
|
|
101
|
+
heading: 'بانتظار موافقة ولي الأمر',
|
|
102
|
+
greeting: 'مرحبًا،',
|
|
103
|
+
body1: (issuerName, credentialName) => (
|
|
104
|
+
<>
|
|
105
|
+
أرسل لك <strong>{issuerName}</strong> الشهادة{' '}
|
|
106
|
+
<strong>«{credentialName}»</strong>.
|
|
107
|
+
</>
|
|
108
|
+
),
|
|
109
|
+
body2: b =>
|
|
110
|
+
`قبل أن تتمكن من المطالبة بها، يجب على ولي أمرك مراجعتها والموافقة عليها أولاً. لقد أرسلنا إليه إشعارًا بالفعل \u2014 بمجرد موافقته، ستتلقى رسالة متابعة وستظهر الشهادة في حسابك على ${b}.`,
|
|
111
|
+
statusBadge: 'بانتظار موافقة ولي الأمر',
|
|
112
|
+
muted: 'لا حاجة لأي إجراء منك في الوقت الحالي.',
|
|
113
|
+
sincerely: 'مع التحيات،',
|
|
114
|
+
subject: 'شهادتك بانتظار موافقة ولي الأمر',
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
30
118
|
export const CredentialAwaitingGuardian: React.FC<CredentialAwaitingGuardianProps> = ({
|
|
31
119
|
branding,
|
|
32
120
|
issuer,
|
|
33
121
|
credential,
|
|
122
|
+
locale,
|
|
34
123
|
}) => {
|
|
124
|
+
const s = STRINGS[resolveCatalogLocale(locale)];
|
|
35
125
|
const issuerName = issuer?.name ?? 'An issuer';
|
|
36
126
|
const credentialName = credential?.name ?? 'a credential';
|
|
37
127
|
|
|
38
128
|
return (
|
|
39
|
-
<Layout
|
|
40
|
-
|
|
129
|
+
<Layout
|
|
130
|
+
branding={branding}
|
|
131
|
+
locale={locale}
|
|
132
|
+
preview={s.preview(credentialName)}
|
|
133
|
+
showHeaderLogo={false}
|
|
134
|
+
>
|
|
135
|
+
<IssuerLogo
|
|
136
|
+
logoUrl={issuer?.logoUrl}
|
|
137
|
+
alt={issuerName ? `${issuerName} logo` : undefined}
|
|
138
|
+
/>
|
|
41
139
|
|
|
42
|
-
<Text style={heading}>
|
|
140
|
+
<Text style={heading}>{s.heading}</Text>
|
|
43
141
|
|
|
44
|
-
<Text style={paragraph}>
|
|
142
|
+
<Text style={paragraph}>{s.greeting}</Text>
|
|
45
143
|
|
|
46
|
-
<Text style={paragraph}>
|
|
47
|
-
<strong>{issuerName}</strong> has sent you the credential{' '}
|
|
48
|
-
<strong>“{credentialName}”</strong>.
|
|
49
|
-
</Text>
|
|
144
|
+
<Text style={paragraph}>{s.body1(issuerName, credentialName)}</Text>
|
|
50
145
|
|
|
51
|
-
<Text style={paragraph}>
|
|
52
|
-
Before you can claim it, your guardian needs to review and approve it first.
|
|
53
|
-
We've already sent them a notification — once they approve, you'll receive
|
|
54
|
-
a follow-up email and the credential will appear in your {branding.brandName} account.
|
|
55
|
-
</Text>
|
|
146
|
+
<Text style={paragraph}>{s.body2(branding.brandName)}</Text>
|
|
56
147
|
|
|
57
148
|
<Section style={statusBadge}>
|
|
58
|
-
<Text style={statusText}>
|
|
149
|
+
<Text style={statusText}>{s.statusBadge}</Text>
|
|
59
150
|
</Section>
|
|
60
151
|
|
|
61
|
-
<Text style={muted}>
|
|
62
|
-
No action is needed from you right now.
|
|
63
|
-
</Text>
|
|
152
|
+
<Text style={muted}>{s.muted}</Text>
|
|
64
153
|
|
|
65
154
|
<Text style={signOff}>
|
|
66
|
-
|
|
155
|
+
{s.sincerely}
|
|
156
|
+
<br />
|
|
157
|
+
{interpolate(SHARED[resolveCatalogLocale(locale)].teamSignature, {
|
|
158
|
+
brandName: branding.brandName,
|
|
159
|
+
})}
|
|
67
160
|
</Text>
|
|
68
161
|
</Layout>
|
|
69
162
|
);
|
|
@@ -71,7 +164,8 @@ export const CredentialAwaitingGuardian: React.FC<CredentialAwaitingGuardianProp
|
|
|
71
164
|
|
|
72
165
|
export const getCredentialAwaitingGuardianSubject = (
|
|
73
166
|
_branding: TenantBranding,
|
|
74
|
-
|
|
167
|
+
locale?: string
|
|
168
|
+
): string => STRINGS[resolveCatalogLocale(locale)].subject;
|
|
75
169
|
|
|
76
170
|
// ---------------------------------------------------------------------------
|
|
77
171
|
// Styles
|