@learncard/email-templates 1.0.4 → 1.0.6

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 (94) hide show
  1. package/dist/branding.d.ts.map +1 -1
  2. package/dist/components/Layout.d.ts +2 -0
  3. package/dist/components/Layout.d.ts.map +1 -1
  4. package/dist/components/Layout.js +6 -1
  5. package/dist/components/Layout.js.map +1 -1
  6. package/dist/components/LinkFallback.d.ts +2 -0
  7. package/dist/components/LinkFallback.d.ts.map +1 -1
  8. package/dist/components/LinkFallback.js +5 -1
  9. package/dist/components/LinkFallback.js.map +1 -1
  10. package/dist/i18n.d.ts +52 -0
  11. package/dist/i18n.d.ts.map +1 -0
  12. package/dist/i18n.js +77 -0
  13. package/dist/i18n.js.map +1 -0
  14. package/dist/index.d.ts +4 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/render.d.ts +3 -1
  19. package/dist/render.d.ts.map +1 -1
  20. package/dist/render.js +33 -30
  21. package/dist/render.js.map +1 -1
  22. package/dist/sms.d.ts +6 -3
  23. package/dist/sms.d.ts.map +1 -1
  24. package/dist/sms.js +43 -7
  25. package/dist/sms.js.map +1 -1
  26. package/dist/templates/account-approved.d.ts +3 -1
  27. package/dist/templates/account-approved.d.ts.map +1 -1
  28. package/dist/templates/account-approved.js +55 -5
  29. package/dist/templates/account-approved.js.map +1 -1
  30. package/dist/templates/credential-awaiting-guardian.d.ts +3 -1
  31. package/dist/templates/credential-awaiting-guardian.d.ts.map +1 -1
  32. package/dist/templates/credential-awaiting-guardian.js +54 -4
  33. package/dist/templates/credential-awaiting-guardian.js.map +1 -1
  34. package/dist/templates/email-verification.d.ts +3 -1
  35. package/dist/templates/email-verification.d.ts.map +1 -1
  36. package/dist/templates/email-verification.js +57 -4
  37. package/dist/templates/email-verification.js.map +1 -1
  38. package/dist/templates/endorsement-request.d.ts +3 -1
  39. package/dist/templates/endorsement-request.d.ts.map +1 -1
  40. package/dist/templates/endorsement-request.js +53 -5
  41. package/dist/templates/endorsement-request.js.map +1 -1
  42. package/dist/templates/guardian-approval.d.ts +3 -1
  43. package/dist/templates/guardian-approval.d.ts.map +1 -1
  44. package/dist/templates/guardian-approval.js +55 -5
  45. package/dist/templates/guardian-approval.js.map +1 -1
  46. package/dist/templates/guardian-approved-claim.d.ts +3 -1
  47. package/dist/templates/guardian-approved-claim.d.ts.map +1 -1
  48. package/dist/templates/guardian-approved-claim.js +50 -4
  49. package/dist/templates/guardian-approved-claim.js.map +1 -1
  50. package/dist/templates/guardian-credential-approval.d.ts +3 -1
  51. package/dist/templates/guardian-credential-approval.d.ts.map +1 -1
  52. package/dist/templates/guardian-credential-approval.js +50 -4
  53. package/dist/templates/guardian-credential-approval.js.map +1 -1
  54. package/dist/templates/guardian-email-otp.d.ts +3 -1
  55. package/dist/templates/guardian-email-otp.d.ts.map +1 -1
  56. package/dist/templates/guardian-email-otp.js +55 -5
  57. package/dist/templates/guardian-email-otp.js.map +1 -1
  58. package/dist/templates/guardian-rejected-credential.d.ts +3 -1
  59. package/dist/templates/guardian-rejected-credential.d.ts.map +1 -1
  60. package/dist/templates/guardian-rejected-credential.js +45 -3
  61. package/dist/templates/guardian-rejected-credential.js.map +1 -1
  62. package/dist/templates/inbox-claim.d.ts +3 -1
  63. package/dist/templates/inbox-claim.d.ts.map +1 -1
  64. package/dist/templates/inbox-claim.js +63 -11
  65. package/dist/templates/inbox-claim.js.map +1 -1
  66. package/dist/templates/recovery-key.d.ts +3 -1
  67. package/dist/templates/recovery-key.d.ts.map +1 -1
  68. package/dist/templates/recovery-key.js +59 -3
  69. package/dist/templates/recovery-key.js.map +1 -1
  70. package/dist/templates/verification-code.d.ts +3 -1
  71. package/dist/templates/verification-code.d.ts.map +1 -1
  72. package/dist/templates/verification-code.js +153 -17
  73. package/dist/templates/verification-code.js.map +1 -1
  74. package/dist/tenant-registry.d.ts.map +1 -1
  75. package/package.json +4 -3
  76. package/src/__tests__/i18n.test.ts +329 -0
  77. package/src/components/Layout.tsx +69 -60
  78. package/src/components/LinkFallback.tsx +19 -11
  79. package/src/i18n.ts +110 -0
  80. package/src/index.ts +15 -2
  81. package/src/render.ts +47 -28
  82. package/src/sms.ts +61 -6
  83. package/src/templates/account-approved.tsx +82 -29
  84. package/src/templates/credential-awaiting-guardian.tsx +113 -19
  85. package/src/templates/email-verification.tsx +87 -16
  86. package/src/templates/endorsement-request.tsx +142 -17
  87. package/src/templates/guardian-approval.tsx +106 -16
  88. package/src/templates/guardian-approved-claim.tsx +137 -17
  89. package/src/templates/guardian-credential-approval.tsx +112 -17
  90. package/src/templates/guardian-email-otp.tsx +103 -25
  91. package/src/templates/guardian-rejected-credential.tsx +137 -15
  92. package/src/templates/inbox-claim.tsx +180 -29
  93. package/src/templates/recovery-key.tsx +126 -30
  94. package/src/templates/verification-code.tsx +204 -38
@@ -17,6 +17,8 @@ import type { TenantBranding } from '../branding';
17
17
  import { DEFAULT_BRANDING } from '../branding';
18
18
  import { Layout } from '../components/Layout';
19
19
  import { CodeBlock } from '../components/CodeBlock';
20
+ import type { NotificationLocale } from '../i18n';
21
+ import { resolveCatalogLocale, interpolate, SHARED } from '../i18n';
20
22
 
21
23
  export type VerificationCodeVariant =
22
24
  | 'login'
@@ -29,29 +31,33 @@ export interface VerificationCodeProps {
29
31
  verificationCode: string;
30
32
  verificationEmail?: string;
31
33
  variant: VerificationCodeVariant;
34
+ /** Recipient locale (BCP-47). Defaults to English. */
35
+ locale?: string;
32
36
  }
33
37
 
34
- const VARIANT_CONFIG: Record<
35
- VerificationCodeVariant,
36
- {
37
- subject: (brandName: string) => string;
38
- heading: (brandName: string) => string;
39
- description: (email: string | undefined, brandName: string) => string;
40
- expiry: string;
41
- }
42
- > = {
38
+ /**
39
+ * Per-variant, per-locale strings. `description` is a function because the
40
+ * login/recovery-email/embed-verification variants historically branched on
41
+ * whether a verification email address was present (two distinct EN strings).
42
+ * EN values byte-match the original templates.
43
+ */
44
+ type VariantStrings = {
45
+ subject: string;
46
+ heading: string;
47
+ description: (email: string | undefined, brandName: string) => string;
48
+ expiry: string;
49
+ };
50
+
51
+ const EN: Record<VerificationCodeVariant, VariantStrings> = {
43
52
  login: {
44
- subject: b => `Your ${b} login code`,
45
- heading: b => `Your ${b} Login Code`,
46
- description: (email, b) =>
47
- email
48
- ? `Here's your secure 6-digit code to log into ${b}`
49
- : `Here's your secure 6-digit code to log into ${b}`,
53
+ subject: 'Your {brandName} login code',
54
+ heading: 'Your {brandName} Login Code',
55
+ description: (_email, b) => `Here's your secure 6-digit code to log into ${b}`,
50
56
  expiry: 'This code will expire in 5 minutes. If you didn\u2019t request this, you can safely ignore this email.',
51
57
  },
52
58
  'recovery-email': {
53
- subject: () => 'Verify your recovery email',
54
- heading: b => `Your ${b} Verification Code`,
59
+ subject: 'Verify your recovery email',
60
+ heading: 'Your {brandName} Verification Code',
55
61
  description: (email, b) =>
56
62
  email
57
63
  ? `Here's your secure 6-digit code to add ${email} as a recovery method for ${b}`
@@ -59,8 +65,8 @@ const VARIANT_CONFIG: Record<
59
65
  expiry: 'This code will expire in 5 minutes. If you didn\u2019t request this, you can safely ignore this email.',
60
66
  },
61
67
  'embed-verification': {
62
- subject: b => `Your ${b} verification code`,
63
- heading: b => `Your ${b} Verification Code`,
68
+ subject: 'Your {brandName} verification code',
69
+ heading: 'Your {brandName} Verification Code',
64
70
  description: email =>
65
71
  email
66
72
  ? `Enter this code to verify ${email} and claim your credential:`
@@ -68,44 +74,200 @@ const VARIANT_CONFIG: Record<
68
74
  expiry: 'This code will expire in 10 minutes. If you didn\u2019t request this, you can safely ignore this email.',
69
75
  },
70
76
  'contact-method': {
71
- subject: b => `Your ${b} verification code`,
72
- heading: b => `Your ${b} Verification Code`,
77
+ subject: 'Your {brandName} verification code',
78
+ heading: 'Your {brandName} Verification Code',
73
79
  description: () => 'Enter this code in the app to verify your contact information.',
74
80
  expiry: 'This code will expire in 24 hours. If you didn\u2019t request this, you can safely ignore this email.',
75
81
  },
76
82
  };
77
83
 
84
+ const ES: Record<VerificationCodeVariant, VariantStrings> = {
85
+ login: {
86
+ subject: 'Tu código de acceso a {brandName}',
87
+ heading: 'Tu código de acceso a {brandName}',
88
+ description: (_email, b) =>
89
+ `Aquí tienes tu código seguro de 6 dígitos para iniciar sesión en ${b}`,
90
+ expiry: 'Este código caducará en 5 minutos. Si no solicitaste esto, puedes ignorar este correo con seguridad.',
91
+ },
92
+ 'recovery-email': {
93
+ subject: 'Verifica tu correo de recuperación',
94
+ heading: 'Tu código de verificación de {brandName}',
95
+ description: (email, b) =>
96
+ email
97
+ ? `Aquí tienes tu código seguro de 6 dígitos para añadir ${email} como método de recuperación de ${b}`
98
+ : `Aquí tienes tu código seguro de 6 dígitos para añadir un método de recuperación de ${b}`,
99
+ expiry: 'Este código caducará en 5 minutos. Si no solicitaste esto, puedes ignorar este correo con seguridad.',
100
+ },
101
+ 'embed-verification': {
102
+ subject: 'Tu código de verificación de {brandName}',
103
+ heading: 'Tu código de verificación de {brandName}',
104
+ description: email =>
105
+ email
106
+ ? `Introduce este código para verificar ${email} y reclamar tu credencial:`
107
+ : 'Introduce este código para verificar tu dirección de correo y reclamar tu credencial:',
108
+ expiry: 'Este código caducará en 10 minutos. Si no solicitaste esto, puedes ignorar este correo con seguridad.',
109
+ },
110
+ 'contact-method': {
111
+ subject: 'Tu código de verificación de {brandName}',
112
+ heading: 'Tu código de verificación de {brandName}',
113
+ description: () =>
114
+ 'Introduce este código en la app para verificar tu información de contacto.',
115
+ expiry: 'Este código caducará en 24 horas. Si no solicitaste esto, puedes ignorar este correo con seguridad.',
116
+ },
117
+ };
118
+
119
+ const FR: Record<VerificationCodeVariant, VariantStrings> = {
120
+ login: {
121
+ subject: 'Votre code de connexion {brandName}',
122
+ heading: 'Votre code de connexion {brandName}',
123
+ description: (_email, b) =>
124
+ `Voici votre code sécurisé à 6 chiffres pour vous connecter à ${b}`,
125
+ expiry: 'Ce code expirera dans 5 minutes. Si vous n\u2019êtes pas à l\u2019origine de cette demande, vous pouvez ignorer cet e-mail en toute sécurité.',
126
+ },
127
+ 'recovery-email': {
128
+ subject: 'Vérifiez votre e-mail de récupération',
129
+ heading: 'Votre code de vérification {brandName}',
130
+ description: (email, b) =>
131
+ email
132
+ ? `Voici votre code sécurisé à 6 chiffres pour ajouter ${email} comme méthode de récupération à ${b}`
133
+ : `Voici votre code sécurisé à 6 chiffres pour ajouter une méthode de récupération à ${b}`,
134
+ expiry: 'Ce code expirera dans 5 minutes. Si vous n\u2019êtes pas à l\u2019origine de cette demande, vous pouvez ignorer cet e-mail en toute sécurité.',
135
+ },
136
+ 'embed-verification': {
137
+ subject: 'Votre code de vérification {brandName}',
138
+ heading: 'Votre code de vérification {brandName}',
139
+ description: email =>
140
+ email
141
+ ? `Saisissez ce code pour vérifier ${email} et réclamer votre titre :`
142
+ : 'Saisissez ce code pour vérifier votre adresse e-mail et réclamer votre titre :',
143
+ expiry: 'Ce code expirera dans 10 minutes. Si vous n\u2019êtes pas à l\u2019origine de cette demande, vous pouvez ignorer cet e-mail en toute sécurité.',
144
+ },
145
+ 'contact-method': {
146
+ subject: 'Votre code de vérification {brandName}',
147
+ heading: 'Votre code de vérification {brandName}',
148
+ description: () =>
149
+ 'Saisissez ce code dans l\u2019application pour vérifier vos coordonnées.',
150
+ expiry: 'Ce code expirera dans 24 heures. Si vous n\u2019êtes pas à l\u2019origine de cette demande, vous pouvez ignorer cet e-mail en toute sécurité.',
151
+ },
152
+ };
153
+
154
+ const AR: Record<VerificationCodeVariant, VariantStrings> = {
155
+ login: {
156
+ subject: 'رمز تسجيل الدخول إلى {brandName}',
157
+ heading: 'رمز تسجيل الدخول إلى {brandName}',
158
+ description: (_email, b) => `إليك رمزك الآمن المكوّن من 6 أرقام لتسجيل الدخول إلى ${b}`,
159
+ expiry: 'تنتهي صلاحية هذا الرمز خلال 5 دقائق. إذا لم تطلب ذلك، يمكنك تجاهل هذه الرسالة بأمان.',
160
+ },
161
+ 'recovery-email': {
162
+ subject: 'تأكيد بريد الاستعادة الخاص بك',
163
+ heading: 'رمز التحقق الخاص بك في {brandName}',
164
+ description: (email, b) =>
165
+ email
166
+ ? `إليك رمزك الآمن المكوّن من 6 أرقام لإضافة ${email} كطريقة استعادة في ${b}`
167
+ : `إليك رمزك الآمن المكوّن من 6 أرقام لإضافة طريقة استعادة في ${b}`,
168
+ expiry: 'تنتهي صلاحية هذا الرمز خلال 5 دقائق. إذا لم تطلب ذلك، يمكنك تجاهل هذه الرسالة بأمان.',
169
+ },
170
+ 'embed-verification': {
171
+ subject: 'رمز التحقق الخاص بك في {brandName}',
172
+ heading: 'رمز التحقق الخاص بك في {brandName}',
173
+ description: email =>
174
+ email
175
+ ? `أدخل هذا الرمز لتأكيد ${email} والمطالبة بشهادتك:`
176
+ : 'أدخل هذا الرمز لتأكيد عنوان بريدك الإلكتروني والمطالبة بشهادتك:',
177
+ expiry: 'تنتهي صلاحية هذا الرمز خلال 10 دقائق. إذا لم تطلب ذلك، يمكنك تجاهل هذه الرسالة بأمان.',
178
+ },
179
+ 'contact-method': {
180
+ subject: 'رمز التحقق الخاص بك في {brandName}',
181
+ heading: 'رمز التحقق الخاص بك في {brandName}',
182
+ description: () => 'أدخل هذا الرمز في التطبيق لتأكيد معلومات الاتصال الخاصة بك.',
183
+ expiry: 'تنتهي صلاحية هذا الرمز خلال 24 ساعة. إذا لم تطلب ذلك، يمكنك تجاهل هذه الرسالة بأمان.',
184
+ },
185
+ };
186
+
187
+ const CATALOGS: Record<NotificationLocale, Record<VerificationCodeVariant, VariantStrings>> = {
188
+ en: EN,
189
+ es: ES,
190
+ fr: FR,
191
+ ar: AR,
192
+ };
193
+
194
+ /** Shared body strings (interpolates {brandName}). */
195
+ const BODY: Record<
196
+ NotificationLocale,
197
+ { preview: string; greeting: string; passport: string; sincerely: string; codeLabel: string }
198
+ > = {
199
+ en: {
200
+ preview: 'Your code: {code}',
201
+ greeting: 'Hello,',
202
+ passport:
203
+ '{brandName} is your private, digital passport for learning and work. It lets you securely collect and share your verified skills and achievements online.',
204
+ sincerely: 'Sincerely,',
205
+ codeLabel: 'Verification code',
206
+ },
207
+ es: {
208
+ preview: 'Tu código: {code}',
209
+ greeting: 'Hola,',
210
+ passport:
211
+ '{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.',
212
+ sincerely: 'Atentamente,',
213
+ codeLabel: 'Código de verificación',
214
+ },
215
+ fr: {
216
+ preview: 'Votre code : {code}',
217
+ greeting: 'Bonjour,',
218
+ passport:
219
+ '{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.',
220
+ sincerely: 'Cordialement,',
221
+ codeLabel: 'Code de vérification',
222
+ },
223
+ ar: {
224
+ preview: 'رمزك: {code}',
225
+ greeting: 'مرحبًا،',
226
+ passport:
227
+ '{brandName} هو جواز سفرك الرقمي والخاص للتعلّم والعمل. يتيح لك جمع ومشاركة مهاراتك وإنجازاتك الموثّقة بأمان عبر الإنترنت.',
228
+ sincerely: 'مع التحيات،',
229
+ codeLabel: 'رمز التحقق',
230
+ },
231
+ };
232
+
78
233
  export const VerificationCode: React.FC<VerificationCodeProps> = ({
79
234
  branding,
80
235
  verificationCode,
81
236
  verificationEmail,
82
237
  variant,
238
+ locale,
83
239
  }) => {
84
- const config = VARIANT_CONFIG[variant];
240
+ const loc = resolveCatalogLocale(locale);
241
+ const config = CATALOGS[loc][variant];
242
+ const body = BODY[loc];
243
+ const heading = interpolate(config.heading, { brandName: branding.brandName });
244
+ const description = config.description(verificationEmail, branding.brandName);
245
+ const passport = interpolate(body.passport, { brandName: branding.brandName });
85
246
 
86
247
  return (
87
- <Layout branding={branding} preview={`Your code: ${verificationCode}`}>
88
- <Text style={headingStyle}>{config.heading(branding.brandName)}</Text>
248
+ <Layout
249
+ branding={branding}
250
+ locale={locale}
251
+ preview={interpolate(body.preview, { code: verificationCode })}
252
+ >
253
+ <Text style={headingStyle}>{heading}</Text>
89
254
 
90
- <Text style={paragraph}>Hello,</Text>
255
+ <Text style={paragraph}>{body.greeting}</Text>
91
256
 
92
- <Text style={paragraph}>
93
- {config.description(verificationEmail, branding.brandName)}
94
- </Text>
257
+ <Text style={paragraph}>{description}</Text>
95
258
 
96
- <CodeBlock code={verificationCode} label="Verification code" />
259
+ <CodeBlock code={verificationCode} label={body.codeLabel} />
97
260
 
98
261
  <Text style={muted}>{config.expiry}</Text>
99
262
 
100
- <Text style={paragraph}>
101
- {branding.brandName} is your private, digital passport for learning and work. It
102
- lets you securely collect and share your verified skills and achievements online.
103
- </Text>
263
+ <Text style={paragraph}>{passport}</Text>
104
264
 
105
265
  <Text style={signOff}>
106
- Sincerely,
266
+ {body.sincerely}
107
267
  <br />
108
- The {branding.brandName} Team
268
+ {interpolate(SHARED[resolveCatalogLocale(locale)].teamSignature, {
269
+ brandName: branding.brandName,
270
+ })}
109
271
  </Text>
110
272
  </Layout>
111
273
  );
@@ -113,8 +275,12 @@ export const VerificationCode: React.FC<VerificationCodeProps> = ({
113
275
 
114
276
  export const getVerificationCodeSubject = (
115
277
  branding: TenantBranding,
116
- variant: VerificationCodeVariant
117
- ): string => VARIANT_CONFIG[variant].subject(branding.brandName);
278
+ variant: VerificationCodeVariant,
279
+ locale?: string
280
+ ): string => {
281
+ const config = CATALOGS[resolveCatalogLocale(locale)][variant];
282
+ return interpolate(config.subject, { brandName: branding.brandName });
283
+ };
118
284
 
119
285
  // ---------------------------------------------------------------------------
120
286
  // Styles
@@ -149,7 +315,7 @@ const signOff: React.CSSProperties = {
149
315
  };
150
316
 
151
317
  // ---------------------------------------------------------------------------
152
- // Preview (used by `bun run dev` / react-email dev server)
318
+ // Preview (used by `pnpm dev` / react-email dev server)
153
319
  // ---------------------------------------------------------------------------
154
320
 
155
321
  export default function Preview() {