feat(emails): localize templates to 7 locales and add newsletter double opt-in #158

Merged
ellamizuki merged 1 commit from feat/email-i18n-and-newsletter-double-opt-in into main 2026-09-09 02:17:01 +00:00
ellamizuki commented 2026-09-09 02:09:29 +00:00 (Migrated from github.com)

Summary

Closes the two scope gaps flagged in the project audit: transactional email templates now cover all seven website locales, and the newsletter uses double opt-in.

Email localization

  • New packages/emails/src/locales.ts: EmailLocale type covering en, pt, es, ja, zh-CN, fr, de, resolve_email_locale() (normalizes BCP-47 input, zh-* -> zh-CN, unknown -> en), plus EMAIL_LANG for <Html lang> and shared greetings.
  • All six localized templates (password-reset, email-verification, account-deletion, comment, reply, contact-confirmation) migrated from the en|pt ternary to a typed copy_by_locale dictionary with hand-written translations for all seven locales. contact-form has no recipient-facing copy and stays as-is.
  • get_request_locale() (auth emails, comment notifications, password reset) now returns EmailLocale, so transactional emails follow the visitor's locale across the full site.
  • New NewsletterConfirmation template (7 locales).

Newsletter double opt-in

POST /api/newsletter/subscribe no longer writes to the Resend audience. Instead it:

  1. Generates a single-use token, stores token -> email in Redis with a 24h TTL.
  2. Sends a localized confirmation email with the confirm URL.

GET /api/newsletter/confirm?token=... validates the token, then adds the contact, re-enables a previously unsubscribed contact, or short-circuits as already-subscribed, and burns the token. Nobody can subscribe a third-party address on their behalf, and typo'd or hostile signups no longer pollute the audience.

Tests

  • newsletter-subscribe-route.test.ts: rewritten for double opt-in — asserts a token is stored with 24h TTL and that addContact/findContactByEmail are never called on subscribe.
  • newsletter-confirm-route.test.ts: new — missing/unknown token 400, success adds contact and burns token, re-subscribe of an unsubscribed contact, idempotent already-subscribed case.

Validation

pnpm check:ci   # exit 0
pnpm test:unit  # 66 files, 283 tests passed

Residual risks

  • The confirmation token is 24h single-use; a lost email requires re-subscribing (standard).
  • Existing unconfirmed double opt-in flows do not exist in prior data, so no migration is needed.
## Summary Closes the two scope gaps flagged in the project audit: transactional email templates now cover all seven website locales, and the newsletter uses double opt-in. ## Email localization - New `packages/emails/src/locales.ts`: `EmailLocale` type covering `en, pt, es, ja, zh-CN, fr, de`, `resolve_email_locale()` (normalizes BCP-47 input, `zh-*` -> `zh-CN`, unknown -> `en`), plus `EMAIL_LANG` for `<Html lang>` and shared greetings. - All six localized templates (password-reset, email-verification, account-deletion, comment, reply, contact-confirmation) migrated from the `en|pt` ternary to a typed `copy_by_locale` dictionary with hand-written translations for all seven locales. `contact-form` has no recipient-facing copy and stays as-is. - `get_request_locale()` (auth emails, comment notifications, password reset) now returns `EmailLocale`, so transactional emails follow the visitor's locale across the full site. - New `NewsletterConfirmation` template (7 locales). ## Newsletter double opt-in `POST /api/newsletter/subscribe` no longer writes to the Resend audience. Instead it: 1. Generates a single-use token, stores `token -> email` in Redis with a 24h TTL. 2. Sends a localized confirmation email with the confirm URL. `GET /api/newsletter/confirm?token=...` validates the token, then adds the contact, re-enables a previously unsubscribed contact, or short-circuits as already-subscribed, and burns the token. Nobody can subscribe a third-party address on their behalf, and typo'd or hostile signups no longer pollute the audience. ## Tests - `newsletter-subscribe-route.test.ts`: rewritten for double opt-in — asserts a token is stored with 24h TTL and that `addContact`/`findContactByEmail` are never called on subscribe. - `newsletter-confirm-route.test.ts`: new — missing/unknown token 400, success adds contact and burns token, re-subscribe of an unsubscribed contact, idempotent already-subscribed case. ## Validation ``` pnpm check:ci # exit 0 pnpm test:unit # 66 files, 283 tests passed ``` ## Residual risks - The confirmation token is 24h single-use; a lost email requires re-subscribing (standard). - Existing unconfirmed double opt-in flows do not exist in prior data, so no migration is needed.
isyuricunha (Migrated from github.com) reviewed 2026-09-09 02:09:29 +00:00
vercel[bot] commented 2026-09-09 02:09:32 +00:00 (Migrated from github.com)

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
website-docs Ready Ready Preview Sep 9, 2026 2:15am UTC
website-web Ready Ready Preview Sep 9, 2026 2:15am UTC
[vc]: #fhyqEKyf5ssHGT7Ymju6ZjS28mla+ldFRw0PIog3tcU=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ3ZWJzaXRlLWRvY3MiLCJwcm9qZWN0SWQiOiJwcmpfMmVVMnNUTTBpZFk3MHJUYnNjQ0N0RXQ2TUZPdyIsInJvb3REaXJlY3RvcnkiOiJhcHBzL2RvY3MiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20veXVyaS1jdW5oYXMtcHJvamVjdHMvd2Vic2l0ZS1kb2NzLzNDOWRZQ2ZzeFdVS3FlRUZpUlVjbTR6ZnlyeFIiLCJwcmV2aWV3VXJsIjoid2Vic2l0ZS1kb2NzLWdpdC1mZWF0LWVtYWlsLWkxOG4tYW4tMzcyNzlmLXl1cmktY3VuaGFzLXByb2plY3RzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoid2Vic2l0ZS1kb2NzLWdpdC1mZWF0LWVtYWlsLWkxOG4tYW4tMzcyNzlmLXl1cmktY3VuaGFzLXByb2plY3RzLnZlcmNlbC5hcHAifX0seyJuYW1lIjoid2Vic2l0ZS13ZWIiLCJwcm9qZWN0SWQiOiJwcmpfYjZyRmhBQVBISVhSNzNjZW9kRTB5dmxCdzM4MiIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3dlYiIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS95dXJpLWN1bmhhcy1wcm9qZWN0cy93ZWJzaXRlLXdlYi83UFlNSFFEQjUzRTNxZHRaTEU4QzlQNUtCbW90IiwicHJldmlld1VybCI6IndlYnNpdGUtd2ViLWdpdC1mZWF0LWVtYWlsLWkxOG4tYW5kLTk1OTNlMi15dXJpLWN1bmhhcy1wcm9qZWN0cy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IndlYnNpdGUtd2ViLWdpdC1mZWF0LWVtYWlsLWkxOG4tYW5kLTk1OTNlMi15dXJpLWN1bmhhcy1wcm9qZWN0cy52ZXJjZWwuYXBwIn19XX0= The latest updates on your projects. Learn more about [Vercel for GitHub](https://vercel.link/github-learn-more). | Project | Deployment | Actions | Updated | | :--- | :----- | :------ | :------ | | <a href="https://vercel.com/yuri-cunhas-projects/website-docs"><sup><img src="https://vercel.com/api/www/avatar?projectId=prj_2eU2sTM0idY70rTbscCCtEt6MFOw&teamId=team_g087rqPZJa9s4DWPeZtQhxa9&s=32" width="16" height="16" align="middle" alt="" /></sup></a> [website-docs](https://vercel.com/yuri-cunhas-projects/website-docs) | ![Ready](https://vercel.com/static/status/ready.svg) [Ready](https://vercel.com/yuri-cunhas-projects/website-docs/3C9dYCfsxWUKqeEFiRUcm4zfyrxR) | [Preview](https://website-docs-git-feat-email-i18n-an-37279f-yuri-cunhas-projects.vercel.app) | <relative-time datetime="2026-09-09T02:15:27.680Z">Sep 9, 2026 2:15am UTC</relative-time> | | <a href="https://vercel.com/yuri-cunhas-projects/website-web"><sup><img src="https://vercel.com/api/www/avatar?projectId=prj_b6rFhAAPHIXR73ceodE0yvlBw382&teamId=team_g087rqPZJa9s4DWPeZtQhxa9&s=32" width="16" height="16" align="middle" alt="" /></sup></a> [website-web](https://vercel.com/yuri-cunhas-projects/website-web) | ![Ready](https://vercel.com/static/status/ready.svg) [Ready](https://vercel.com/yuri-cunhas-projects/website-web/7PYMHQDB53E3qdtZLE8C9P5KBmot) | [Preview](https://website-web-git-feat-email-i18n-and-9593e2-yuri-cunhas-projects.vercel.app) | <relative-time datetime="2026-09-09T02:15:27.680Z">Sep 9, 2026 2:15am UTC</relative-time> |
Sign in to join this conversation.
No description provided.