package account import ( "fmt" "html/template" "strings" tmpltext "text/template" "time" ) // emailBrandColor is the single accent used in the confirmation email — a calm // tile green, matching the "no riot of colours" brief. const emailBrandColor = "#2f7d4f" // confirmEmailView is the fully-localised data the confirmation email templates // render. Every string is resolved before rendering, so the templates carry no // localisation logic. type confirmEmailView struct { Brand string Heading string Intro string Code string Expiry string CTALabel string DeeplinkURL string FooterIgnore string LandingURL string LandingLabel string Preheader string Locale string Accent string } // emailCopy is the purpose- and locale-specific wording of a confirmation email. type emailCopy struct { Subject string Preheader string Heading string Intro string CTALabel string FooterIgnore string } // confirmEmailCopy holds the wording per (purpose, locale). Unknown purposes fall // back to the neutral link wording and unknown locales fall back to English. var confirmEmailCopy = map[string]map[string]emailCopy{ purposeLogin: { "en": { Subject: "Your Erudit sign-in code", Preheader: "Your sign-in code", Heading: "Sign in to Erudit", Intro: "Enter this code to sign in:", CTALabel: "Sign in with one tap", FooterIgnore: "If you didn't request this email, you can safely ignore it.", }, "ru": { Subject: "Код для входа в Эрудит", Preheader: "Ваш код для входа", Heading: "Вход в Эрудит", Intro: "Введите этот код, чтобы войти в игру:", CTALabel: "Войти одним нажатием", FooterIgnore: "Если вы не запрашивали это письмо, просто проигнорируйте его.", }, }, purposeLink: { "en": { Subject: "Your Erudit confirmation code", Preheader: "Your confirmation code", Heading: "Confirm your e-mail", Intro: "Enter this code to confirm your address:", CTALabel: "Confirm with one tap", FooterIgnore: "If you didn't request this email, you can safely ignore it.", }, "ru": { Subject: "Код подтверждения Эрудит", Preheader: "Ваш код подтверждения", Heading: "Подтверждение e-mail", Intro: "Введите этот код, чтобы подтвердить адрес:", CTALabel: "Подтвердить одним нажатием", FooterIgnore: "Если вы не запрашивали это письмо, просто проигнорируйте его.", }, }, purposeChange: { "en": { Subject: "Confirm your new Erudit e-mail", Preheader: "Confirm your new address", Heading: "Confirm your new e-mail", Intro: "Enter this code to switch your account to this address:", CTALabel: "Confirm with one tap", FooterIgnore: "If you didn't request this change, you can safely ignore it — your address stays the same.", }, "ru": { Subject: "Подтвердите новый e-mail в Эрудит", Preheader: "Подтвердите новый адрес", Heading: "Смена e-mail", Intro: "Введите этот код, чтобы привязать аккаунт к новому адресу:", CTALabel: "Подтвердить одним нажатием", FooterIgnore: "Если вы не запрашивали смену, просто проигнорируйте письмо — адрес останется прежним.", }, }, purposeDelete: { "en": { Subject: "Confirm your Erudit account deletion", Preheader: "Confirm account deletion", Heading: "Delete your account", Intro: "Enter this code in the app to permanently delete your account:", CTALabel: "", FooterIgnore: "If you didn't request this, ignore it — your account stays as it is.", }, "ru": { Subject: "Подтвердите удаление аккаунта Эрудит", Preheader: "Подтверждение удаления аккаунта", Heading: "Удаление аккаунта", Intro: "Введите этот код в приложении, чтобы удалить аккаунт без восстановления:", CTALabel: "", FooterIgnore: "Если вы не запрашивали удаление, проигнорируйте письмо — аккаунт останется.", }, }, } // emailBrand is the brand wordmark per locale. var emailBrand = map[string]string{"en": "Erudit", "ru": "Эрудит"} // emailExpiry formats the code-lifetime line per locale (abbreviated minutes to // avoid plural agreement). func emailExpiry(locale string, d time.Duration) string { min := int(d / time.Minute) if locale == "ru" { return fmt.Sprintf("Код действует %d мин.", min) } return fmt.Sprintf("The code is valid for %d minutes.", min) } // normalizeLocale maps an account language to a supported email locale, defaulting // to English. func normalizeLocale(locale string) string { if locale == "ru" { return "ru" } return "en" } // renderConfirmationEmail builds the branded confirmation email for purpose in // locale: a large readable code plus a one-tap deeplink button, with an // ignore-notice footer and a landing link. Both a plain-text body and an HTML // alternative are produced. deeplinkURL is the absolute /confirm link and // landingURL the public landing origin. func renderConfirmationEmail(purpose, code, deeplinkURL, landingURL, locale string) (Message, error) { loc := normalizeLocale(locale) byLocale, ok := confirmEmailCopy[purpose] if !ok { byLocale = confirmEmailCopy[purposeLink] } cp := byLocale[loc] view := confirmEmailView{ Brand: emailBrand[loc], Heading: cp.Heading, Intro: cp.Intro, Code: code, Expiry: emailExpiry(loc, emailCodeTTL), CTALabel: cp.CTALabel, DeeplinkURL: deeplinkURL, FooterIgnore: cp.FooterIgnore, LandingURL: landingURL, LandingLabel: emailBrand[loc], Preheader: cp.Preheader, Locale: loc, Accent: emailBrandColor, } var html strings.Builder if err := confirmEmailHTML.Execute(&html, view); err != nil { return Message{}, fmt.Errorf("account: render confirmation email (html): %w", err) } var text strings.Builder if err := confirmEmailText.Execute(&text, view); err != nil { return Message{}, fmt.Errorf("account: render confirmation email (text): %w", err) } return Message{Subject: cp.Subject, Text: text.String(), HTML: html.String()}, nil } // confirmEmailHTML is a compact, image-free, mobile-friendly HTML email. Layout is // table-based for broad mail-client compatibility and all styling is inlined // because clients strip