waitlist-invite.js
3.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import { EmailError } from '../map-error.js'
const COPY = Object.freeze({
en: {
subject: 'Your VOKO invite',
heading: 'Your VOKO invite is ready',
intro: 'Use the link below to finish registration.',
action: 'Finish registration',
warning: 'This invite link expires and should not be shared.',
},
zh: {
subject: '您的 VOKO 邀请',
heading: '您的 VOKO 邀请已准备就绪',
intro: '请使用下方链接完成注册。',
action: '完成注册',
warning: '该邀请链接会过期,请勿分享给他人。',
},
ja: {
subject: 'VOKOへの招待',
heading: 'VOKOへの招待の準備ができました',
intro: '以下のリンクから登録を完了してください。',
action: '登録を完了',
warning: 'この招待リンクには有効期限があります。他の人と共有しないでください。',
},
ko: {
subject: 'VOKO 초대',
heading: 'VOKO 초대가 준비되었습니다',
intro: '아래 링크를 사용해 가입을 완료하세요.',
action: '가입 완료',
warning: '이 초대 링크는 만료되며 다른 사람과 공유하면 안 됩니다.',
},
es: {
subject: 'Tu invitación a VOKO',
heading: 'Tu invitación a VOKO está lista',
intro: 'Usa el siguiente enlace para completar el registro.',
action: 'Completar registro',
warning: 'Este enlace de invitación caduca y no debe compartirse.',
},
pt: {
subject: 'Seu convite para o VOKO',
heading: 'Seu convite para o VOKO está pronto',
intro: 'Use o link abaixo para concluir o cadastro.',
action: 'Concluir cadastro',
warning: 'Este link de convite expira e não deve ser compartilhado.',
},
})
/**
* @param {{ locale: string, inviteUrl: string }} data
*/
export function render(data) {
if (!data || typeof data !== 'object') fail('data is required')
const copy = COPY[data.locale]
if (!copy) fail(`unsupported locale: ${String(data.locale)}`)
const inviteUrl = requiredHttpsUrl(data.inviteUrl)
const text = [copy.heading, '', copy.intro, inviteUrl, '', copy.warning].join('\n')
const html = [
'<!doctype html><html><body style="font-family:Arial,Helvetica,sans-serif;color:#1a1a1a;line-height:1.6;">',
`<h2 style="margin:0 0 16px;">${escapeHtml(copy.heading)}</h2>`,
`<p style="margin:0 0 16px;">${escapeHtml(copy.intro)}</p>`,
`<p style="margin:0 0 16px;"><a href="${escapeHtml(inviteUrl)}">${escapeHtml(copy.action)}</a></p>`,
`<p style="margin:0;color:#666;">${escapeHtml(copy.warning)}</p>`,
'</body></html>',
].join('')
return { subject: copy.subject, text, html }
}
function requiredHttpsUrl(value) {
if (typeof value !== 'string' || value.trim() === '') fail('missing field "inviteUrl"')
const url = value.trim()
try {
const parsed = new URL(url)
// 本地开发的前端是 http://localhost,其余环境必须 https。
if (parsed.protocol === 'https:' || parsed.hostname === 'localhost') return url
} catch (error) {
fail(`inviteUrl is invalid: ${error instanceof Error ? error.message : String(error)}`)
}
fail('inviteUrl must use https')
}
function fail(message) {
throw new EmailError({ provider: 'template', reason: 'badTemplate', retryable: false, message: `waitlist-invite: ${message}` })
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''')
}