@learncard/email-templates 1.0.3 → 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/dist/tenant-registry.d.ts.map +1 -1
- package/dist/tenant-registry.js +17 -0
- package/dist/tenant-registry.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/i18n.test.ts +329 -0
- package/src/__tests__/tenant-registry.test.ts +24 -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/tenant-registry.ts +24 -2
|
@@ -11,6 +11,8 @@ import * as React from 'react';
|
|
|
11
11
|
import type { TenantBranding } from '../branding';
|
|
12
12
|
import { DEFAULT_BRANDING } from '../branding';
|
|
13
13
|
import { Layout } from '../components/Layout';
|
|
14
|
+
import type { NotificationLocale } from '../i18n';
|
|
15
|
+
import { resolveCatalogLocale, interpolate, SHARED } from '../i18n';
|
|
14
16
|
|
|
15
17
|
export interface GuardianRejectedCredentialProps {
|
|
16
18
|
branding: TenantBranding;
|
|
@@ -23,36 +25,155 @@ export interface GuardianRejectedCredentialProps {
|
|
|
23
25
|
recipient?: {
|
|
24
26
|
email?: string;
|
|
25
27
|
};
|
|
28
|
+
/** Recipient locale (BCP-47). Defaults to English. */
|
|
29
|
+
locale?: string;
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
const STRINGS: Record<
|
|
33
|
+
NotificationLocale,
|
|
34
|
+
{
|
|
35
|
+
preview: string;
|
|
36
|
+
heading: string;
|
|
37
|
+
greeting: string;
|
|
38
|
+
body: (issuerName: string, credentialName: string | undefined) => React.ReactNode;
|
|
39
|
+
body2: (issuerName: string) => React.ReactNode;
|
|
40
|
+
sincerely: string;
|
|
41
|
+
subject: string;
|
|
42
|
+
}
|
|
43
|
+
> = {
|
|
44
|
+
en: {
|
|
45
|
+
preview: 'Your guardian has declined a credential',
|
|
46
|
+
heading: 'Credential not approved',
|
|
47
|
+
greeting: 'Hi there,',
|
|
48
|
+
body: (issuerName, credentialName) => (
|
|
49
|
+
<>
|
|
50
|
+
Your guardian has reviewed and <strong>declined</strong> the credential{' '}
|
|
51
|
+
{credentialName ? (
|
|
52
|
+
<>
|
|
53
|
+
<strong>“{credentialName}”</strong>{' '}
|
|
54
|
+
</>
|
|
55
|
+
) : (
|
|
56
|
+
''
|
|
57
|
+
)}
|
|
58
|
+
issued by <strong>{issuerName}</strong>.
|
|
59
|
+
</>
|
|
60
|
+
),
|
|
61
|
+
body2: issuerName => (
|
|
62
|
+
<>
|
|
63
|
+
The credential will not be added to your account. If you think this was a mistake,
|
|
64
|
+
please reach out to your guardian or contact <strong>{issuerName}</strong> directly.
|
|
65
|
+
</>
|
|
66
|
+
),
|
|
67
|
+
sincerely: 'Sincerely,',
|
|
68
|
+
subject: 'Credential Not Approved',
|
|
69
|
+
},
|
|
70
|
+
es: {
|
|
71
|
+
preview: 'Tu tutor ha rechazado una credencial',
|
|
72
|
+
heading: 'Credencial no aprobada',
|
|
73
|
+
greeting: 'Hola,',
|
|
74
|
+
body: (issuerName, credentialName) => (
|
|
75
|
+
<>
|
|
76
|
+
Tu tutor ha revisado y <strong>rechazado</strong> la credencial{' '}
|
|
77
|
+
{credentialName ? (
|
|
78
|
+
<>
|
|
79
|
+
<strong>“{credentialName}”</strong>{' '}
|
|
80
|
+
</>
|
|
81
|
+
) : (
|
|
82
|
+
''
|
|
83
|
+
)}
|
|
84
|
+
emitida por <strong>{issuerName}</strong>.
|
|
85
|
+
</>
|
|
86
|
+
),
|
|
87
|
+
body2: issuerName => (
|
|
88
|
+
<>
|
|
89
|
+
La credencial no se añadirá a tu cuenta. Si crees que esto ha sido un error,
|
|
90
|
+
comunícate con tu tutor o contacta directamente con <strong>{issuerName}</strong>.
|
|
91
|
+
</>
|
|
92
|
+
),
|
|
93
|
+
sincerely: 'Atentamente,',
|
|
94
|
+
subject: 'Credencial no aprobada',
|
|
95
|
+
},
|
|
96
|
+
fr: {
|
|
97
|
+
preview: 'Votre tuteur a refusé un titre',
|
|
98
|
+
heading: 'Titre non approuvé',
|
|
99
|
+
greeting: 'Bonjour,',
|
|
100
|
+
body: (issuerName, credentialName) => (
|
|
101
|
+
<>
|
|
102
|
+
Votre tuteur a examiné et <strong>refusé</strong> le titre{' '}
|
|
103
|
+
{credentialName ? (
|
|
104
|
+
<>
|
|
105
|
+
<strong>« {credentialName} »</strong>{' '}
|
|
106
|
+
</>
|
|
107
|
+
) : (
|
|
108
|
+
''
|
|
109
|
+
)}
|
|
110
|
+
émis par <strong>{issuerName}</strong>.
|
|
111
|
+
</>
|
|
112
|
+
),
|
|
113
|
+
body2: issuerName => (
|
|
114
|
+
<>
|
|
115
|
+
Le titre ne sera pas ajouté à votre compte. Si vous pensez qu\u2019il s\u2019agit
|
|
116
|
+
d\u2019une erreur, veuillez contacter votre tuteur ou <strong>{issuerName}</strong>{' '}
|
|
117
|
+
directement.
|
|
118
|
+
</>
|
|
119
|
+
),
|
|
120
|
+
sincerely: 'Cordialement,',
|
|
121
|
+
subject: 'Titre non approuvé',
|
|
122
|
+
},
|
|
123
|
+
ar: {
|
|
124
|
+
preview: 'رفض ولي أمرك شهادة',
|
|
125
|
+
heading: 'لم تتم الموافقة على الشهادة',
|
|
126
|
+
greeting: 'مرحبًا،',
|
|
127
|
+
body: (issuerName, credentialName) => (
|
|
128
|
+
<>
|
|
129
|
+
راجع ولي أمرك و<strong>رفض</strong> الشهادة{' '}
|
|
130
|
+
{credentialName ? (
|
|
131
|
+
<>
|
|
132
|
+
<strong>«{credentialName}»</strong>{' '}
|
|
133
|
+
</>
|
|
134
|
+
) : (
|
|
135
|
+
''
|
|
136
|
+
)}
|
|
137
|
+
الصادرة من <strong>{issuerName}</strong>.
|
|
138
|
+
</>
|
|
139
|
+
),
|
|
140
|
+
body2: issuerName => (
|
|
141
|
+
<>
|
|
142
|
+
لن تُضاف الشهادة إلى حسابك. إذا كنت تعتقد أن هذا خطأ، يرجى التواصل مع ولي أمرك أو
|
|
143
|
+
الاتصال بـ <strong>{issuerName}</strong> مباشرة.
|
|
144
|
+
</>
|
|
145
|
+
),
|
|
146
|
+
sincerely: 'مع التحيات،',
|
|
147
|
+
subject: 'لم تتم الموافقة على الشهادة',
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
28
151
|
export const GuardianRejectedCredential: React.FC<GuardianRejectedCredentialProps> = ({
|
|
29
152
|
branding,
|
|
30
153
|
issuer,
|
|
31
154
|
credential,
|
|
155
|
+
locale,
|
|
32
156
|
}) => {
|
|
157
|
+
const s = STRINGS[resolveCatalogLocale(locale)];
|
|
33
158
|
const issuerName = issuer?.name ?? 'An issuer';
|
|
34
159
|
const credentialName = credential?.name;
|
|
35
160
|
|
|
36
161
|
return (
|
|
37
|
-
<Layout branding={branding} preview=
|
|
38
|
-
<Text style={heading}>
|
|
162
|
+
<Layout branding={branding} locale={locale} preview={s.preview}>
|
|
163
|
+
<Text style={heading}>{s.heading}</Text>
|
|
39
164
|
|
|
40
|
-
<Text style={paragraph}>
|
|
165
|
+
<Text style={paragraph}>{s.greeting}</Text>
|
|
41
166
|
|
|
42
|
-
<Text style={paragraph}>
|
|
43
|
-
Your guardian has reviewed and <strong>declined</strong> the credential{' '}
|
|
44
|
-
{credentialName ? <><strong>“{credentialName}”</strong> </> : ''}
|
|
45
|
-
issued by <strong>{issuerName}</strong>.
|
|
46
|
-
</Text>
|
|
167
|
+
<Text style={paragraph}>{s.body(issuerName, credentialName)}</Text>
|
|
47
168
|
|
|
48
|
-
<Text style={paragraph}>
|
|
49
|
-
The credential will not be added to your account. If you think this was
|
|
50
|
-
a mistake, please reach out to your guardian or contact{' '}
|
|
51
|
-
<strong>{issuerName}</strong> directly.
|
|
52
|
-
</Text>
|
|
169
|
+
<Text style={paragraph}>{s.body2(issuerName)}</Text>
|
|
53
170
|
|
|
54
171
|
<Text style={signOff}>
|
|
55
|
-
|
|
172
|
+
{s.sincerely}
|
|
173
|
+
<br />
|
|
174
|
+
{interpolate(SHARED[resolveCatalogLocale(locale)].teamSignature, {
|
|
175
|
+
brandName: branding.brandName,
|
|
176
|
+
})}
|
|
56
177
|
</Text>
|
|
57
178
|
</Layout>
|
|
58
179
|
);
|
|
@@ -60,7 +181,8 @@ export const GuardianRejectedCredential: React.FC<GuardianRejectedCredentialProp
|
|
|
60
181
|
|
|
61
182
|
export const getGuardianRejectedCredentialSubject = (
|
|
62
183
|
_branding: TenantBranding,
|
|
63
|
-
|
|
184
|
+
locale?: string
|
|
185
|
+
): string => STRINGS[resolveCatalogLocale(locale)].subject;
|
|
64
186
|
|
|
65
187
|
// ---------------------------------------------------------------------------
|
|
66
188
|
// Styles
|
|
@@ -13,6 +13,8 @@ import { Layout } from '../components/Layout';
|
|
|
13
13
|
import { EmailButton } from '../components/EmailButton';
|
|
14
14
|
import { IssuerLogo } from '../components/IssuerLogo';
|
|
15
15
|
import { LinkFallback } from '../components/LinkFallback';
|
|
16
|
+
import type { NotificationLocale } from '../i18n';
|
|
17
|
+
import { resolveCatalogLocale, interpolate, SHARED } from '../i18n';
|
|
16
18
|
|
|
17
19
|
export interface InboxClaimProps {
|
|
18
20
|
branding: TenantBranding;
|
|
@@ -30,58 +32,207 @@ export interface InboxClaimProps {
|
|
|
30
32
|
name?: string;
|
|
31
33
|
type?: string;
|
|
32
34
|
};
|
|
35
|
+
/** Recipient locale (BCP-47). Defaults to English. */
|
|
36
|
+
locale?: string;
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Intro is a function returning ReactNode because it conditionally wraps the
|
|
41
|
+
* issuer name and credential name in <strong> + curly quotes, and word order
|
|
42
|
+
* shifts per locale. EN reproduces the original JSX exactly.
|
|
43
|
+
*/
|
|
44
|
+
type Strings = {
|
|
45
|
+
greeting: string;
|
|
46
|
+
greetingName: string;
|
|
47
|
+
heading: (credentialType: string) => string;
|
|
48
|
+
preview: (credentialType: string, issuerName?: string) => string;
|
|
49
|
+
intro: (issuerName: string | undefined, credentialName: string | undefined) => React.ReactNode;
|
|
50
|
+
passport: (brandName: string, credentialType: string) => React.ReactNode;
|
|
51
|
+
button: string;
|
|
52
|
+
sincerely: string;
|
|
53
|
+
subject: (issuerName?: string) => string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const ldquo = '\u201C';
|
|
57
|
+
const rdquo = '\u201D';
|
|
58
|
+
const mdash = '\u2014';
|
|
59
|
+
|
|
60
|
+
const EN: Strings = {
|
|
61
|
+
greeting: 'Hello,',
|
|
62
|
+
greetingName: 'Hello {name},',
|
|
63
|
+
heading: t => `Your digital ${t} is ready`,
|
|
64
|
+
preview: (t, issuer) => `Your digital ${t}${issuer ? ` from ${issuer}` : ''} is ready`,
|
|
65
|
+
intro: (issuerName, credentialName) => (
|
|
66
|
+
<>
|
|
67
|
+
{issuerName ? (
|
|
68
|
+
<>
|
|
69
|
+
<strong>{issuerName}</strong> has sent you
|
|
70
|
+
</>
|
|
71
|
+
) : (
|
|
72
|
+
<>You've received</>
|
|
73
|
+
)}{' '}
|
|
74
|
+
a secure, digital version of your{' '}
|
|
75
|
+
<strong>{credentialName ? `${ldquo}${credentialName}${rdquo}` : 'achievement'}</strong>.
|
|
76
|
+
</>
|
|
77
|
+
),
|
|
78
|
+
passport: (b, t) => (
|
|
79
|
+
<>
|
|
80
|
+
{b} is your private, digital passport for learning and work. It lets you securely
|
|
81
|
+
collect and share your verified skills and achievements online. By claiming this item,
|
|
82
|
+
you are saving a verifiable, official {t} to your personal passport{mdash}one that only
|
|
83
|
+
you control.
|
|
84
|
+
</>
|
|
85
|
+
),
|
|
86
|
+
button: 'Claim Your Record \u2192',
|
|
87
|
+
sincerely: 'Sincerely,',
|
|
88
|
+
subject: issuer => `Your digital record ${issuer ? `from ${issuer} ` : ''}is ready`,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const ES: Strings = {
|
|
92
|
+
greeting: 'Hola,',
|
|
93
|
+
greetingName: 'Hola {name},',
|
|
94
|
+
heading: t => `Tu ${t} digital está listo`,
|
|
95
|
+
preview: (t, issuer) => `Tu ${t} digital${issuer ? ` de ${issuer}` : ''} está listo`,
|
|
96
|
+
intro: (issuerName, credentialName) => (
|
|
97
|
+
<>
|
|
98
|
+
{issuerName ? (
|
|
99
|
+
<>
|
|
100
|
+
<strong>{issuerName}</strong> te ha enviado
|
|
101
|
+
</>
|
|
102
|
+
) : (
|
|
103
|
+
<>Has recibido</>
|
|
104
|
+
)}{' '}
|
|
105
|
+
una versión digital y segura de tu{' '}
|
|
106
|
+
<strong>{credentialName ? `${ldquo}${credentialName}${rdquo}` : 'logro'}</strong>.
|
|
107
|
+
</>
|
|
108
|
+
),
|
|
109
|
+
passport: (b, t) => (
|
|
110
|
+
<>
|
|
111
|
+
{b} es tu pasaporte digital y privado para el aprendizaje y el trabajo. Te permite
|
|
112
|
+
recopilar y compartir de forma segura tus habilidades y logros verificados en línea. Al
|
|
113
|
+
reclamar este elemento, estás guardando un {t}
|
|
114
|
+
verificable y oficial en tu pasaporte personal{mdash}uno que solo tú controlas.
|
|
115
|
+
</>
|
|
116
|
+
),
|
|
117
|
+
button: 'Reclamar tu registro \u2192',
|
|
118
|
+
sincerely: 'Atentamente,',
|
|
119
|
+
subject: issuer => `Tu registro digital ${issuer ? `de ${issuer} ` : ''}está listo`,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const FR: Strings = {
|
|
123
|
+
greeting: 'Bonjour,',
|
|
124
|
+
greetingName: 'Bonjour {name},',
|
|
125
|
+
heading: t => `Votre ${t} numérique est prêt`,
|
|
126
|
+
preview: (t, issuer) => `Votre ${t} numérique${issuer ? ` de ${issuer}` : ''} est prêt`,
|
|
127
|
+
intro: (issuerName, credentialName) => (
|
|
128
|
+
<>
|
|
129
|
+
{issuerName ? (
|
|
130
|
+
<>
|
|
131
|
+
<strong>{issuerName}</strong> vous a envoyé
|
|
132
|
+
</>
|
|
133
|
+
) : (
|
|
134
|
+
<>Vous avez reçu</>
|
|
135
|
+
)}{' '}
|
|
136
|
+
une version numérique et sécurisée de votre{' '}
|
|
137
|
+
<strong>{credentialName ? `${ldquo}${credentialName}${rdquo}` : 'réalisation'}</strong>.
|
|
138
|
+
</>
|
|
139
|
+
),
|
|
140
|
+
passport: (b, t) => (
|
|
141
|
+
<>
|
|
142
|
+
{b} est votre passeport numérique privé pour l\u2019apprentissage et le travail. Il vous
|
|
143
|
+
permet de collecter et de partager en toute sécurité vos compétences et réalisations
|
|
144
|
+
vérifiées en ligne. En réclamant cet élément, vous enregistrez un {t} vérifiable et
|
|
145
|
+
officiel dans votre passeport personnel{mdash}que vous seul contrôlez.
|
|
146
|
+
</>
|
|
147
|
+
),
|
|
148
|
+
button: 'Réclamer votre titre \u2192',
|
|
149
|
+
sincerely: 'Cordialement,',
|
|
150
|
+
subject: issuer => `Votre titre numérique ${issuer ? `de ${issuer} ` : ''}est prêt`,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const AR: Strings = {
|
|
154
|
+
greeting: 'مرحبًا،',
|
|
155
|
+
greetingName: 'مرحبًا {name}،',
|
|
156
|
+
heading: t => `${t}ك الرقمي جاهز`,
|
|
157
|
+
preview: (t, issuer) => `${t}ك الرقمي${issuer ? ` من ${issuer}` : ''} جاهز`,
|
|
158
|
+
intro: (issuerName, credentialName) => (
|
|
159
|
+
<>
|
|
160
|
+
{issuerName ? (
|
|
161
|
+
<>
|
|
162
|
+
أرسل لك <strong>{issuerName}</strong>
|
|
163
|
+
</>
|
|
164
|
+
) : (
|
|
165
|
+
<>لقد استلمت</>
|
|
166
|
+
)}{' '}
|
|
167
|
+
نسخة رقمية وآمنة من{' '}
|
|
168
|
+
<strong>{credentialName ? `${ldquo}${credentialName}${rdquo}` : 'إنجازك'}</strong>.
|
|
169
|
+
</>
|
|
170
|
+
),
|
|
171
|
+
passport: (b, t) => (
|
|
172
|
+
<>
|
|
173
|
+
{b} هو جواز سفرك الرقمي والخاص للتعلّم والعمل. يتيح لك جمع ومشاركة مهاراتك وإنجازاتك
|
|
174
|
+
الموثّقة بأمان عبر الإنترنت. من خلال المطالبة بهذا العنصر، فأنت تحفظ {t}ًا قابلاً للتحقق
|
|
175
|
+
ورسميًا في جواز سفرك الشخصي{mdash}الذي تتحكم فيه أنت وحدك.
|
|
176
|
+
</>
|
|
177
|
+
),
|
|
178
|
+
button: 'مطالبة سجلك ←',
|
|
179
|
+
sincerely: 'مع التحيات،',
|
|
180
|
+
subject: issuer => `سجلك الرقمي ${issuer ? `من ${issuer} ` : ''}جاهز`,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const STRINGS: Record<NotificationLocale, Strings> = { en: EN, es: ES, fr: FR, ar: AR };
|
|
184
|
+
|
|
35
185
|
export const InboxClaim: React.FC<InboxClaimProps> = ({
|
|
36
186
|
branding,
|
|
37
187
|
claimUrl,
|
|
38
188
|
recipient,
|
|
39
189
|
issuer,
|
|
40
190
|
credential,
|
|
191
|
+
locale,
|
|
41
192
|
}) => {
|
|
193
|
+
const s = STRINGS[resolveCatalogLocale(locale)];
|
|
42
194
|
const credentialName = credential?.name;
|
|
43
195
|
const credentialType = credential?.type ?? 'record';
|
|
44
|
-
const greeting = recipient?.name ? `Hello ${recipient.name},` : 'Hello,';
|
|
45
196
|
const issuerName = issuer?.name;
|
|
46
197
|
const issuerLogo = issuer?.logoUrl;
|
|
47
198
|
const showHeaderLogo = !issuerLogo;
|
|
48
|
-
|
|
199
|
+
|
|
200
|
+
const greeting = interpolate(recipient?.name ? s.greetingName : s.greeting, {
|
|
201
|
+
name: recipient?.name,
|
|
202
|
+
});
|
|
203
|
+
|
|
49
204
|
return (
|
|
50
|
-
<Layout
|
|
205
|
+
<Layout
|
|
206
|
+
branding={branding}
|
|
207
|
+
locale={locale}
|
|
208
|
+
preview={s.preview(credentialType, issuerName)}
|
|
209
|
+
showHeaderLogo={showHeaderLogo}
|
|
210
|
+
>
|
|
51
211
|
<IssuerLogo logoUrl={issuerLogo} alt={issuerName ? `${issuerName} logo` : undefined} />
|
|
52
212
|
|
|
53
|
-
<Text style={heading}>
|
|
213
|
+
<Text style={heading}>{s.heading(credentialType)}</Text>
|
|
54
214
|
|
|
55
|
-
<Text style={paragraph}>
|
|
56
|
-
{greeting}
|
|
57
|
-
</Text>
|
|
215
|
+
<Text style={paragraph}>{greeting}</Text>
|
|
58
216
|
|
|
59
|
-
<Text style={paragraph}>
|
|
60
|
-
{issuerName
|
|
61
|
-
? <><strong>{issuerName}</strong> has sent you</>
|
|
62
|
-
: <>You've received</>}{' '}
|
|
63
|
-
a secure, digital version of your{' '}
|
|
64
|
-
<strong>{credentialName ? `\u201C${credentialName}\u201D` : 'achievement'}</strong>.
|
|
65
|
-
</Text>
|
|
217
|
+
<Text style={paragraph}>{s.intro(issuerName, credentialName)}</Text>
|
|
66
218
|
|
|
67
|
-
<Text style={paragraph}>
|
|
68
|
-
{branding.brandName} is your private, digital passport for learning and work.
|
|
69
|
-
It lets you securely collect and share your verified skills and achievements
|
|
70
|
-
online. By claiming this item, you are saving a verifiable, official {credentialType} to
|
|
71
|
-
your personal passport—one that only you control.
|
|
72
|
-
</Text>
|
|
219
|
+
<Text style={paragraph}>{s.passport(branding.brandName, credentialType)}</Text>
|
|
73
220
|
|
|
74
221
|
<Section style={buttonWrapper}>
|
|
75
222
|
<EmailButton href={claimUrl} branding={branding}>
|
|
76
|
-
|
|
223
|
+
{s.button}
|
|
77
224
|
</EmailButton>
|
|
78
225
|
</Section>
|
|
79
226
|
|
|
80
227
|
<Text style={signOff}>
|
|
81
|
-
|
|
228
|
+
{s.sincerely}
|
|
229
|
+
<br />
|
|
230
|
+
{interpolate(SHARED[resolveCatalogLocale(locale)].teamSignature, {
|
|
231
|
+
brandName: branding.brandName,
|
|
232
|
+
})}
|
|
82
233
|
</Text>
|
|
83
234
|
|
|
84
|
-
<LinkFallback href={claimUrl} />
|
|
235
|
+
<LinkFallback href={claimUrl} locale={locale} />
|
|
85
236
|
</Layout>
|
|
86
237
|
);
|
|
87
238
|
};
|
|
@@ -89,11 +240,8 @@ export const InboxClaim: React.FC<InboxClaimProps> = ({
|
|
|
89
240
|
export const getInboxClaimSubject = (
|
|
90
241
|
_branding: TenantBranding,
|
|
91
242
|
props: InboxClaimProps,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return `Your digital record ${issuerPart}is ready`;
|
|
96
|
-
};
|
|
243
|
+
locale?: string
|
|
244
|
+
): string => STRINGS[resolveCatalogLocale(locale)].subject(props.issuer?.name);
|
|
97
245
|
|
|
98
246
|
// ---------------------------------------------------------------------------
|
|
99
247
|
// Styles
|
|
@@ -134,7 +282,10 @@ export default function Preview() {
|
|
|
134
282
|
branding={DEFAULT_BRANDING}
|
|
135
283
|
claimUrl="https://learncard.app/claim/abc123"
|
|
136
284
|
recipient={{ name: 'Jane Doe' }}
|
|
137
|
-
issuer={{
|
|
285
|
+
issuer={{
|
|
286
|
+
name: 'Acme University',
|
|
287
|
+
logoUrl: 'https://cdn.filestackcontent.com/J6suaVcQ467W9o1k48Kj',
|
|
288
|
+
}}
|
|
138
289
|
credential={{ name: 'Bachelor of Science', type: 'Achievement' }}
|
|
139
290
|
/>
|
|
140
291
|
);
|
|
@@ -11,50 +11,146 @@ import type { TenantBranding } from '../branding';
|
|
|
11
11
|
import { DEFAULT_BRANDING } from '../branding';
|
|
12
12
|
import { Layout } from '../components/Layout';
|
|
13
13
|
import { CodeBlock } from '../components/CodeBlock';
|
|
14
|
+
import type { NotificationLocale } from '../i18n';
|
|
15
|
+
import { resolveCatalogLocale, interpolate, SHARED } from '../i18n';
|
|
14
16
|
|
|
15
17
|
export interface RecoveryKeyProps {
|
|
16
18
|
branding: TenantBranding;
|
|
17
19
|
recoveryKey: string;
|
|
20
|
+
/** Recipient locale (BCP-47). Defaults to English. */
|
|
21
|
+
locale?: string;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const STRINGS: Record<
|
|
25
|
+
NotificationLocale,
|
|
26
|
+
{
|
|
27
|
+
preview: (brandName: string) => string;
|
|
28
|
+
heading: (brandName: string) => string;
|
|
29
|
+
greeting: string;
|
|
30
|
+
body: (brandName: string) => string;
|
|
31
|
+
codeLabel: string;
|
|
32
|
+
recoverInstruction: () => React.ReactNode;
|
|
33
|
+
notRequested: string;
|
|
34
|
+
passport: string;
|
|
35
|
+
sincerely: string;
|
|
36
|
+
subject: (brandName: string) => string;
|
|
37
|
+
}
|
|
38
|
+
> = {
|
|
39
|
+
en: {
|
|
40
|
+
preview: b => `Your ${b} recovery key \u2014 keep this safe`,
|
|
41
|
+
heading: b => `Your ${b} Recovery Key`,
|
|
42
|
+
greeting: 'Hello,',
|
|
43
|
+
body: b =>
|
|
44
|
+
`Keep this email safe. You can use the recovery key below to regain access to your ${b} account if you lose your device.`,
|
|
45
|
+
codeLabel: 'Recovery Key \u2014 do NOT share this with anyone',
|
|
46
|
+
recoverInstruction: () => (
|
|
47
|
+
<>
|
|
48
|
+
To recover your account, choose <strong>“Recover via Email”</strong> in
|
|
49
|
+
the app and paste the recovery key above when prompted.
|
|
50
|
+
</>
|
|
51
|
+
),
|
|
52
|
+
notRequested: 'If you did not request this, you can safely ignore this email.',
|
|
53
|
+
passport:
|
|
54
|
+
'{brandName} is your private, digital passport for learning and work. It lets you securely collect and share your verified skills and achievements online.',
|
|
55
|
+
sincerely: 'Sincerely,',
|
|
56
|
+
subject: b => `Your ${b} recovery key`,
|
|
57
|
+
},
|
|
58
|
+
es: {
|
|
59
|
+
preview: b => `Tu clave de recuperación de ${b} \u2014 guárdala en un lugar seguro`,
|
|
60
|
+
heading: b => `Tu clave de recuperación de ${b}`,
|
|
61
|
+
greeting: 'Hola,',
|
|
62
|
+
body: b =>
|
|
63
|
+
`Guarda este correo en un lugar seguro. Puedes usar la clave de recuperación siguiente para recuperar el acceso a tu cuenta de ${b} si pierdes tu dispositivo.`,
|
|
64
|
+
codeLabel: 'Clave de recuperación \u2014 NO la compartas con nadie',
|
|
65
|
+
recoverInstruction: () => (
|
|
66
|
+
<>
|
|
67
|
+
Para recuperar tu cuenta, elige <strong>“Recuperar por correo”</strong>{' '}
|
|
68
|
+
en la app y pega la clave de recuperación anterior cuando se te solicite.
|
|
69
|
+
</>
|
|
70
|
+
),
|
|
71
|
+
notRequested: 'Si no solicitaste esto, puedes ignorar este correo con seguridad.',
|
|
72
|
+
passport:
|
|
73
|
+
'{brandName} es tu pasaporte digital y privado para el aprendizaje y el trabajo. Te permite recopilar y compartir de forma segura tus habilidades y logros verificados en línea.',
|
|
74
|
+
sincerely: 'Atentamente,',
|
|
75
|
+
subject: b => `Tu clave de recuperación de ${b}`,
|
|
76
|
+
},
|
|
77
|
+
fr: {
|
|
78
|
+
preview: b => `Votre clé de récupération ${b} \u2014 conservez-la en sécurité`,
|
|
79
|
+
heading: b => `Votre clé de récupération ${b}`,
|
|
80
|
+
greeting: 'Bonjour,',
|
|
81
|
+
body: b =>
|
|
82
|
+
`Conservez cet e-mail en sécurité. Vous pouvez utiliser la clé de récupération ci-dessous pour retrouver l\u2019accès à votre compte ${b} si vous perdez votre appareil.`,
|
|
83
|
+
codeLabel: 'Clé de récupération \u2014 NE la partagez avec PERSONNE',
|
|
84
|
+
recoverInstruction: () => (
|
|
85
|
+
<>
|
|
86
|
+
Pour récupérer votre compte, choisissez{' '}
|
|
87
|
+
<strong>« Récupérer par e-mail »</strong> dans
|
|
88
|
+
l\u2019application et collez la clé de récupération ci-dessus lorsque cela vous est
|
|
89
|
+
demandé.
|
|
90
|
+
</>
|
|
91
|
+
),
|
|
92
|
+
notRequested:
|
|
93
|
+
'Si vous n\u2019êtes pas à l\u2019origine de cette demande, vous pouvez ignorer cet e-mail en toute sécurité.',
|
|
94
|
+
passport:
|
|
95
|
+
'{brandName} est votre passeport numérique privé pour l\u2019apprentissage et le travail. Il vous permet de collecter et de partager en toute sécurité vos compétences et réalisations vérifiées en ligne.',
|
|
96
|
+
sincerely: 'Cordialement,',
|
|
97
|
+
subject: b => `Votre clé de récupération ${b}`,
|
|
98
|
+
},
|
|
99
|
+
ar: {
|
|
100
|
+
preview: b => `مفتاح الاستعادة الخاص بك في ${b} \u2014 احتفظ به بأمان`,
|
|
101
|
+
heading: b => `مفتاح الاستعادة الخاص بك في ${b}`,
|
|
102
|
+
greeting: 'مرحبًا،',
|
|
103
|
+
body: b =>
|
|
104
|
+
`احتفظ بهذا البريد في مكان آمن. يمكنك استخدام مفتاح الاستعادة أدناه لاستعادة الوصول إلى حسابك في ${b} إذا فقدت جهازك.`,
|
|
105
|
+
codeLabel: 'مفتاح الاستعادة \u2014 لا تشاركه مع أي شخص إطلاقًا',
|
|
106
|
+
recoverInstruction: () => (
|
|
107
|
+
<>
|
|
108
|
+
لاستعادة حسابك، اختر <strong>«الاستعادة عبر البريد»</strong> في التطبيق
|
|
109
|
+
والصق مفتاح الاستعادة أعلاه عند المطالبة بذلك.
|
|
110
|
+
</>
|
|
111
|
+
),
|
|
112
|
+
notRequested: 'إذا لم تطلب ذلك، يمكنك تجاهل هذه الرسالة بأمان.',
|
|
113
|
+
passport:
|
|
114
|
+
'{brandName} هو جواز سفرك الرقمي والخاص للتعلّم والعمل. يتيح لك جمع ومشاركة مهاراتك وإنجازاتك الموثّقة بأمان عبر الإنترنت.',
|
|
115
|
+
sincerely: 'مع التحيات،',
|
|
116
|
+
subject: b => `مفتاح الاستعادة الخاص بك في ${b}`,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const RecoveryKey: React.FC<RecoveryKeyProps> = ({ branding, recoveryKey, locale }) => {
|
|
121
|
+
const s = STRINGS[resolveCatalogLocale(locale)];
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Layout branding={branding} locale={locale} preview={s.preview(branding.brandName)}>
|
|
125
|
+
<Text style={heading}>{s.heading(branding.brandName)}</Text>
|
|
26
126
|
|
|
27
|
-
|
|
127
|
+
<Text style={paragraph}>{s.greeting}</Text>
|
|
28
128
|
|
|
29
|
-
|
|
30
|
-
Keep this email safe. You can use the recovery key below to regain access
|
|
31
|
-
to your {branding.brandName} account if you lose your device.
|
|
32
|
-
</Text>
|
|
129
|
+
<Text style={paragraph}>{s.body(branding.brandName)}</Text>
|
|
33
130
|
|
|
34
|
-
|
|
131
|
+
<CodeBlock code={recoveryKey} label={s.codeLabel} variant="key" />
|
|
35
132
|
|
|
36
|
-
|
|
37
|
-
To recover your account, choose <strong>“Recover via Email”</strong> in
|
|
38
|
-
the app and paste the recovery key above when prompted.
|
|
39
|
-
</Text>
|
|
133
|
+
<Text style={paragraph}>{s.recoverInstruction()}</Text>
|
|
40
134
|
|
|
41
|
-
|
|
42
|
-
If you did not request this, you can safely ignore this email.
|
|
43
|
-
</Text>
|
|
135
|
+
<Text style={muted}>{s.notRequested}</Text>
|
|
44
136
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
</Text>
|
|
137
|
+
<Text style={paragraph}>
|
|
138
|
+
{interpolate(s.passport, { brandName: branding.brandName })}
|
|
139
|
+
</Text>
|
|
49
140
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
141
|
+
<Text style={signOff}>
|
|
142
|
+
{s.sincerely}
|
|
143
|
+
<br />
|
|
144
|
+
{interpolate(SHARED[resolveCatalogLocale(locale)].teamSignature, {
|
|
145
|
+
brandName: branding.brandName,
|
|
146
|
+
})}
|
|
147
|
+
</Text>
|
|
148
|
+
</Layout>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
55
151
|
|
|
56
|
-
export const getRecoveryKeySubject = (branding: TenantBranding): string =>
|
|
57
|
-
|
|
152
|
+
export const getRecoveryKeySubject = (branding: TenantBranding, locale?: string): string =>
|
|
153
|
+
STRINGS[resolveCatalogLocale(locale)].subject(branding.brandName);
|
|
58
154
|
|
|
59
155
|
// ---------------------------------------------------------------------------
|
|
60
156
|
// Styles
|