@nomideusz/svelte-notify
Transactional email templates as plain functions: pass the booking, get back { subject, html, text }. Localized in English, Polish and Ukrainian, with
per-call copy overrides so the app keeps its own voice. QR codes come from @nomideusz/svelte-qr.
pnpm add @nomideusz/svelte-notify
text/plain alternative
Your booking is confirmed Hi Anna Kowalska, Your booking for Sunrise Hatha class is confirmed. - Date: Monday, 14 September 2026 at 07:30 - Participants: 2 - Total paid: PLN 120.00 - Reference: BK-4F92A1 Show this QR code at the entrance https://example.com/verify/BK-4F92A1 See you there! -- thebest.travel
Usage
import { bookingConfirmationTemplate } from '@nomideusz/svelte-notify';
const { subject, html, text } = bookingConfirmationTemplate({
guestName: 'Anna Kowalska',
serviceName: 'Sunrise Hatha class',
slotStartTime: new Date('2026-09-14T07:30:00Z'),
participants: 2,
totalAmount: 12000, // cents
currency: 'PLN',
bookingReference: 'BK-4F92A1',
language: 'pl', // 'en' | 'pl' | 'uk'; unknown falls back to English
brand: 'thebest.travel',
verifyUrl: 'https://example.com/verify/BK-4F92A1', // inlines a QR code
});
await sendMail({ to: guest.email, subject, html, text }); The templates are pure functions — no transport, no side effects. Every one returns a text/plain alternative alongside the HTML. Send the result with whatever mailer
you already use, or with the SMTP transport below. Date and money formatting comes from @nomideusz/svelte-i18n (formatDateTime, formatMoney), so pages and emails share one
implementation.
Bind once with createNotifier
import { createNotifier } from '@nomideusz/svelte-notify';
// Once, next to your mailer setup:
const notify = createNotifier({
brand: 'szkolyjogi.pl',
theme: { accent: '#3f6f4f', radius: '6px' },
language: 'pl', // default when a message carries none
timeZone: 'Europe/Warsaw', // pin it — all times render in this zone
overrides: (lang) => VOICE[lang], // house voice, resolved per language
});
// Call sites pass only what varies per message:
notify.bookingConfirmation(data); // also: providerNotification,
notify.reminder(data); // cancellation, scheduleChanged,
notify.actionLink({ ... }); // and the action-link shell below Per-call values win over the config, key by key — language stays per-recipient even when the app has a default.
One-off emails: the shared shell
import { actionLinkTemplate, escapeHtml } from '@nomideusz/svelte-notify';
// Magic link, password reset, invite — the copy is yours, the shell isn't:
const { subject, html, text } = actionLinkTemplate({
subject: 'Sign in to szkolyjogi.pl', // plain text, never HTML
headingHtml: 'Sign in',
bodyHtml: '<p>Hi ' + escapeHtml(user.name) + ', this link signs you in.</p>',
cta: { label: 'Sign in', url: magicLink }, // label & url escaped for you
footnotesHtml: ['The link expires in 15 minutes.'],
brand: 'szkolyjogi.pl',
}); The escaping contract: templates are trusted, params are not. Fields suffixed Html carry deliberate markup and render as-is — run untrusted values (names
typed into public forms) through escapeHtml before composing them in. Plain
fields (brand, the cta) are escaped for you. renderLayout, renderText and htmlToText are exported
too if you want the card without a subject.
Sending: SMTP transport (server-side)
// Server only — needs the optional nodemailer peer: pnpm add nodemailer
import { createMailer } from '@nomideusz/svelte-notify/transport';
const mailer = createMailer({
host: env.SMTP_HOST, // any of host/user/pass missing ⇒ send()
user: env.SMTP_USER, // throws (safe default); pass onUnconfigured
pass: env.SMTP_PASS, // to log-and-skip instead
port: 465, // default; secure defaults to port === 465
from: 'noreply@example.com',
fromName: 'szkolyjogi.pl', // sent as { name, address }
replyTo: 'hello@example.com',
});
await mailer.send({ to: data.guestEmail, ...notify.bookingConfirmation(data) });
await mailer.canSend(); // transporter.verify(), cached for 60 s Transient failures — 4xx SMTP responses and connection errors — are retried with
800 ms then 1600 ms backoff; permanent 5xx is not. Only the /transport subpath imports nodemailer, so the main entry stays pure and
browser-safe.