@moveo-ai/email-templates
v2.0.0
Published
Transactional email templates for the Moveo.AI platform — typed renderer, 3 locales, white-label support.
Readme
@moveo-ai/email-templates
Transactional email templates for the Moveo.AI platform. Type-safe React Email renderer, three locales, dark-mode support, and a white-label brand override.
Install
npm install @moveo-ai/email-templates
# or
pnpm add @moveo-ai/email-templatesPeer dependency: React 18 or 19.
Quick start
import { renderEmail } from '@moveo-ai/email-templates';
const { html, text, subject } = await renderEmail(
'welcome',
{
first_name: 'Sam',
sender_email: '[email protected]',
console_url: 'https://console.eu.moveo.ai',
},
'en'
);renderEmail<Id> is type-safe — passing the wrong props shape for a given
id is a compile error.
Sending an email
The package is render-only — renderEmail returns { subject, html, text }
and your service hands that to a real mail transport. SendGrid example:
import sgMail from '@sendgrid/mail';
import { renderEmail } from '@moveo-ai/email-templates';
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);
const { subject, html, text } = await renderEmail(
'welcome',
{
first_name: 'Sam',
sender_email: '[email protected]',
console_url: 'https://console.eu.moveo.ai',
},
'en'
);
await sgMail.send({
from: { name: 'Moveo.AI', email: '[email protected]' },
to: '[email protected]',
subject,
html,
text,
});Nodemailer, AWS SES, Postmark, Resend, Mailgun — same shape: pass html to
the transport's html field, text to its text field, and subject to
its subject field. Nothing transport-specific lives in the package.
Per-recipient locale
For broadcasts where each recipient may want a different locale, call
renderEmail once per locale and send the right rendered output to each
admin. isLocale narrows an untrusted string to Locale:
import {
DEFAULT_LOCALE,
isLocale,
type Locale,
renderEmail,
} from '@moveo-ai/email-templates';
const resolveLocale = (raw: unknown): Locale =>
isLocale(raw) ? raw : DEFAULT_LOCALE;
for (const admin of admins) {
const locale = resolveLocale(admin.preferences?.language);
const { subject, html, text } = await renderEmail('weekly_analytics', params, locale);
await sgMail.send({ from, to: admin.email, subject, html, text });
}Renders are deterministic for a given (id, props, locale) — caching per
locale is a worthwhile optimization when many recipients share one.
Templates
| id | Purpose |
|---|---|
| welcome | Account creation. Primary CTA → console; secondary onboarding-call CTA. |
| invitation | Workspace invite. Differentiated CTAs for new vs existing accounts; role pills; expiration. |
| conversation_assigned | Hand-off notification for support agents. Channel + customer + last-message preview. |
| request_access | Notifies an account owner that a teammate requested a specific permission. Optional requester message + suggested-roles chip row that already grant the permission. |
| alerts | Real-time or digest alerts. Mixed-priority rows with deep links into the review queue. |
| weekly_analytics | Weekly usage digest. Metric tiles + trend chips. |
| monthly_analytics | Same shape as weekly, monthly cadence. |
TEMPLATE_IDS (a readonly array) and isTemplateId (a type guard) are
exported for callers that need to iterate or validate.
White-label
Pass a brand object on any template's props to render under a different
brand. Every "Moveo.AI" mention switches to brand.name, the logo becomes
brand-name text, Moveo-specific CTAs / attribution are hidden.
await renderEmail(
'welcome',
{
first_name: 'Sam',
sender_email: '[email protected]',
console_url: 'https://app.acme.example.com',
brand: {
name: 'Acme Cloud',
company_address: '500 Market St, San Francisco, CA 94105',
privacy_url: 'https://acme.example.com/privacy',
terms_url: 'https://acme.example.com/terms',
},
},
'en'
);Only brand.name is required. Address / Privacy / Terms URLs are optional —
when omitted, the corresponding footer elements are hidden. Sending without
company_address does not meet CAN-SPAM — consumer's responsibility.
Supported locales
en, es, pt-br. Locale codes are lowercase BCP-47-ish.
Dark mode
Dark-mode rules ship in the rendered HTML and fire on:
@media (prefers-color-scheme: dark)— Apple Mail (mac/iOS), Outlook 2019+ desktop, Thunderbird.[data-ogsc]— Outlook.com webmail and Outlook for Android.
Gmail webmail ignores prefers-color-scheme and applies its own
auto-inversion; the palette is chosen to survive that gracefully (no
pure-black / pure-white).
