@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.
Files changed (97) hide show
  1. package/dist/components/Layout.d.ts +2 -0
  2. package/dist/components/Layout.d.ts.map +1 -1
  3. package/dist/components/Layout.js +6 -1
  4. package/dist/components/Layout.js.map +1 -1
  5. package/dist/components/LinkFallback.d.ts +2 -0
  6. package/dist/components/LinkFallback.d.ts.map +1 -1
  7. package/dist/components/LinkFallback.js +5 -1
  8. package/dist/components/LinkFallback.js.map +1 -1
  9. package/dist/i18n.d.ts +52 -0
  10. package/dist/i18n.d.ts.map +1 -0
  11. package/dist/i18n.js +77 -0
  12. package/dist/i18n.js.map +1 -0
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/render.d.ts +3 -1
  18. package/dist/render.d.ts.map +1 -1
  19. package/dist/render.js +33 -30
  20. package/dist/render.js.map +1 -1
  21. package/dist/sms.d.ts +6 -3
  22. package/dist/sms.d.ts.map +1 -1
  23. package/dist/sms.js +43 -7
  24. package/dist/sms.js.map +1 -1
  25. package/dist/templates/account-approved.d.ts +3 -1
  26. package/dist/templates/account-approved.d.ts.map +1 -1
  27. package/dist/templates/account-approved.js +55 -5
  28. package/dist/templates/account-approved.js.map +1 -1
  29. package/dist/templates/credential-awaiting-guardian.d.ts +3 -1
  30. package/dist/templates/credential-awaiting-guardian.d.ts.map +1 -1
  31. package/dist/templates/credential-awaiting-guardian.js +54 -4
  32. package/dist/templates/credential-awaiting-guardian.js.map +1 -1
  33. package/dist/templates/email-verification.d.ts +3 -1
  34. package/dist/templates/email-verification.d.ts.map +1 -1
  35. package/dist/templates/email-verification.js +57 -4
  36. package/dist/templates/email-verification.js.map +1 -1
  37. package/dist/templates/endorsement-request.d.ts +3 -1
  38. package/dist/templates/endorsement-request.d.ts.map +1 -1
  39. package/dist/templates/endorsement-request.js +53 -5
  40. package/dist/templates/endorsement-request.js.map +1 -1
  41. package/dist/templates/guardian-approval.d.ts +3 -1
  42. package/dist/templates/guardian-approval.d.ts.map +1 -1
  43. package/dist/templates/guardian-approval.js +55 -5
  44. package/dist/templates/guardian-approval.js.map +1 -1
  45. package/dist/templates/guardian-approved-claim.d.ts +3 -1
  46. package/dist/templates/guardian-approved-claim.d.ts.map +1 -1
  47. package/dist/templates/guardian-approved-claim.js +50 -4
  48. package/dist/templates/guardian-approved-claim.js.map +1 -1
  49. package/dist/templates/guardian-credential-approval.d.ts +3 -1
  50. package/dist/templates/guardian-credential-approval.d.ts.map +1 -1
  51. package/dist/templates/guardian-credential-approval.js +50 -4
  52. package/dist/templates/guardian-credential-approval.js.map +1 -1
  53. package/dist/templates/guardian-email-otp.d.ts +3 -1
  54. package/dist/templates/guardian-email-otp.d.ts.map +1 -1
  55. package/dist/templates/guardian-email-otp.js +55 -5
  56. package/dist/templates/guardian-email-otp.js.map +1 -1
  57. package/dist/templates/guardian-rejected-credential.d.ts +3 -1
  58. package/dist/templates/guardian-rejected-credential.d.ts.map +1 -1
  59. package/dist/templates/guardian-rejected-credential.js +45 -3
  60. package/dist/templates/guardian-rejected-credential.js.map +1 -1
  61. package/dist/templates/inbox-claim.d.ts +3 -1
  62. package/dist/templates/inbox-claim.d.ts.map +1 -1
  63. package/dist/templates/inbox-claim.js +63 -11
  64. package/dist/templates/inbox-claim.js.map +1 -1
  65. package/dist/templates/recovery-key.d.ts +3 -1
  66. package/dist/templates/recovery-key.d.ts.map +1 -1
  67. package/dist/templates/recovery-key.js +59 -3
  68. package/dist/templates/recovery-key.js.map +1 -1
  69. package/dist/templates/verification-code.d.ts +3 -1
  70. package/dist/templates/verification-code.d.ts.map +1 -1
  71. package/dist/templates/verification-code.js +153 -17
  72. package/dist/templates/verification-code.js.map +1 -1
  73. package/dist/tenant-registry.d.ts.map +1 -1
  74. package/dist/tenant-registry.js +17 -0
  75. package/dist/tenant-registry.js.map +1 -1
  76. package/package.json +1 -1
  77. package/src/__tests__/i18n.test.ts +329 -0
  78. package/src/__tests__/tenant-registry.test.ts +24 -0
  79. package/src/components/Layout.tsx +69 -60
  80. package/src/components/LinkFallback.tsx +19 -11
  81. package/src/i18n.ts +110 -0
  82. package/src/index.ts +15 -2
  83. package/src/render.ts +47 -28
  84. package/src/sms.ts +61 -6
  85. package/src/templates/account-approved.tsx +82 -29
  86. package/src/templates/credential-awaiting-guardian.tsx +113 -19
  87. package/src/templates/email-verification.tsx +87 -16
  88. package/src/templates/endorsement-request.tsx +142 -17
  89. package/src/templates/guardian-approval.tsx +106 -16
  90. package/src/templates/guardian-approved-claim.tsx +137 -17
  91. package/src/templates/guardian-credential-approval.tsx +112 -17
  92. package/src/templates/guardian-email-otp.tsx +103 -25
  93. package/src/templates/guardian-rejected-credential.tsx +137 -15
  94. package/src/templates/inbox-claim.tsx +180 -29
  95. package/src/templates/recovery-key.tsx +126 -30
  96. package/src/templates/verification-code.tsx +204 -38
  97. 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>&ldquo;{credentialName}&rdquo;</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>&ldquo;{credentialName}&rdquo;</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>&laquo;&nbsp;{credentialName}&nbsp;&raquo;</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>&laquo;{credentialName}&raquo;</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="Your guardian has declined a credential">
38
- <Text style={heading}>Credential not approved</Text>
162
+ <Layout branding={branding} locale={locale} preview={s.preview}>
163
+ <Text style={heading}>{s.heading}</Text>
39
164
 
40
- <Text style={paragraph}>Hi there,</Text>
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>&ldquo;{credentialName}&rdquo;</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
- Sincerely,<br />The {branding.brandName} Team
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
- ): string => 'Credential Not Approved';
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&apos;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 branding={branding} preview={`Your digital ${credentialType}${issuerName ? ` from ${issuerName}` : ''} is ready`} showHeaderLogo={showHeaderLogo}>
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}>Your digital {credentialType} is ready</Text>
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&apos;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&mdash;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
- Claim Your Record &rarr;
223
+ {s.button}
77
224
  </EmailButton>
78
225
  </Section>
79
226
 
80
227
  <Text style={signOff}>
81
- Sincerely,<br />The {branding.brandName} Team
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
- ): string => {
93
- const issuerPart = props.issuer?.name ? `from ${props.issuer.name} ` : '';
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={{ name: 'Acme University', logoUrl: 'https://cdn.filestackcontent.com/J6suaVcQ467W9o1k48Kj' }}
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
- export const RecoveryKey: React.FC<RecoveryKeyProps> = ({
21
- branding,
22
- recoveryKey,
23
- }) => (
24
- <Layout branding={branding} preview={`Your ${branding.brandName} recovery key — keep this safe`}>
25
- <Text style={heading}>Your {branding.brandName} Recovery Key</Text>
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>&ldquo;Recover via Email&rdquo;</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>&ldquo;Recuperar por correo&rdquo;</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>&laquo;&nbsp;Récupérer par e-mail&nbsp;&raquo;</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>&laquo;الاستعادة عبر البريد&raquo;</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
- <Text style={paragraph}>Hello,</Text>
127
+ <Text style={paragraph}>{s.greeting}</Text>
28
128
 
29
- <Text style={paragraph}>
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
- <CodeBlock code={recoveryKey} label="Recovery Key — do NOT share this with anyone" variant="key" />
131
+ <CodeBlock code={recoveryKey} label={s.codeLabel} variant="key" />
35
132
 
36
- <Text style={paragraph}>
37
- To recover your account, choose <strong>&ldquo;Recover via Email&rdquo;</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
- <Text style={muted}>
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
- <Text style={paragraph}>
46
- {branding.brandName} is your private, digital passport for learning and work.
47
- It lets you securely collect and share your verified skills and achievements online.
48
- </Text>
137
+ <Text style={paragraph}>
138
+ {interpolate(s.passport, { brandName: branding.brandName })}
139
+ </Text>
49
140
 
50
- <Text style={signOff}>
51
- Sincerely,<br />The {branding.brandName} Team
52
- </Text>
53
- </Layout>
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
- `Your ${branding.brandName} recovery key`;
152
+ export const getRecoveryKeySubject = (branding: TenantBranding, locale?: string): string =>
153
+ STRINGS[resolveCatalogLocale(locale)].subject(branding.brandName);
58
154
 
59
155
  // ---------------------------------------------------------------------------
60
156
  // Styles