From e4a42c62e8cbabdcadb04e40548e992d798f2dfc Mon Sep 17 00:00:00 2001 From: Ivo Date: Fri, 16 Oct 2026 08:02:53 +0200 Subject: [PATCH 1/8] feat(examples): send Elements emails through 14 providers, and add an i18n example The content-workflows example adds Mailgun, Azure, MailerSend, Scaleway, Plunk, Lettermint, Brevo, Inbound and EUSend, each tested against a mocked endpoint. The new i18n example renders one template in English and Arabic with react-intl, react-i18next and next-intl, right to left included, and runs in CI. Co-Authored-By: Claude Code (Claude Opus 5.5 xhigh) --- .github/workflows/test.yml | 5 + examples/content-workflows/README.md | 4 +- examples/content-workflows/package.json | 25 +- .../content-workflows/src/providers/azure.ts | 16 + .../content-workflows/src/providers/brevo.ts | 13 + .../content-workflows/src/providers/eusend.ts | 7 + .../src/providers/inbound.ts | 7 + .../src/providers/lettermint.ts | 7 + .../src/providers/mailersend.ts | 13 + .../src/providers/mailgun.ts | 19 + .../content-workflows/src/providers/plunk.ts | 20 + .../src/providers/scaleway.ts | 21 + .../content-workflows/src/workflows.test.ts | 182 +++ examples/i18n/.gitignore | 1 + examples/i18n/README.md | 25 + examples/i18n/messages/ar.json | 9 + examples/i18n/messages/en.json | 9 + examples/i18n/package.json | 28 + examples/i18n/src/i18n.test.ts | 59 + examples/i18n/src/locales.ts | 33 + examples/i18n/src/next-intl.tsx | 34 + examples/i18n/src/react-i18next.tsx | 53 + examples/i18n/src/react-intl.tsx | 45 + examples/i18n/src/render.ts | 11 + examples/i18n/tsconfig.json | 8 + pnpm-lock.yaml | 1455 ++++++++++++++++- 26 files changed, 2067 insertions(+), 42 deletions(-) create mode 100644 examples/content-workflows/src/providers/azure.ts create mode 100644 examples/content-workflows/src/providers/brevo.ts create mode 100644 examples/content-workflows/src/providers/eusend.ts create mode 100644 examples/content-workflows/src/providers/inbound.ts create mode 100644 examples/content-workflows/src/providers/lettermint.ts create mode 100644 examples/content-workflows/src/providers/mailersend.ts create mode 100644 examples/content-workflows/src/providers/mailgun.ts create mode 100644 examples/content-workflows/src/providers/plunk.ts create mode 100644 examples/content-workflows/src/providers/scaleway.ts create mode 100644 examples/i18n/.gitignore create mode 100644 examples/i18n/README.md create mode 100644 examples/i18n/messages/ar.json create mode 100644 examples/i18n/messages/en.json create mode 100644 examples/i18n/package.json create mode 100644 examples/i18n/src/i18n.test.ts create mode 100644 examples/i18n/src/locales.ts create mode 100644 examples/i18n/src/next-intl.tsx create mode 100644 examples/i18n/src/react-i18next.tsx create mode 100644 examples/i18n/src/react-intl.tsx create mode 100644 examples/i18n/src/render.ts create mode 100644 examples/i18n/tsconfig.json diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a73ba329..74c421f1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -128,6 +128,11 @@ jobs: pnpm --filter @unlayer/content-workflows-example typecheck pnpm --filter @unlayer/content-workflows-example test + - name: i18n example (react-intl, react-i18next, and next-intl in English and Arabic) + run: | + pnpm --filter @unlayer/i18n-example typecheck + pnpm --filter @unlayer/i18n-example test + # ── Browser import/runtime safety gate ────────────────────── # Bundles the shipped package entry as a browser consumer would, then # imports and renders inside Chromium without a Node `process` global. diff --git a/examples/content-workflows/README.md b/examples/content-workflows/README.md index ab1772c6..2e5006c5 100644 --- a/examples/content-workflows/README.md +++ b/examples/content-workflows/README.md @@ -41,7 +41,7 @@ The [provider guide](../../packages/react/docs/email-providers.md) lists install - `src/message.ts`: HTML and plain-text renderer plus environment handling. - `src/render-email.ts` / `src/render-all.ts`: output files. - `src/pdf.ts`: Playwright PDF generation and browser cleanup. -- `src/providers/`: Resend, SES, Postmark, SendGrid, and SMTP delivery. +- `src/providers/`: Resend, SES, Postmark, SendGrid, SMTP, Mailgun, Azure Communication Services, MailerSend, Scaleway, Plunk, Lettermint, Brevo, Inbound, and eusend delivery. - `editor.html`: wait for editor readiness, load design, export edits. ```bash @@ -49,4 +49,4 @@ pnpm --filter @unlayer/content-workflows-example typecheck pnpm --filter @unlayer/content-workflows-example test ``` -Tests check invoice content in Chromium across all three modes, plain text, editable JSON, escaped customer input, URL validation, and real PDF bytes. They do not send email or depend on the hosted editor. Review inbox compatibility and PDF pagination with your real content before deployment. See the [task guides](../../packages/react/docs/README.md) for constraints and adaptation. +Tests check invoice content in Chromium across all three modes, plain text, editable JSON, escaped customer input, URL validation, and real PDF bytes. They also run the Mailgun, Azure, MailerSend, Scaleway, Plunk, Lettermint, Brevo, Inbound, and eusend scripts with [nock](https://github.com/nock/nock) answering their HTTP requests, and check what each one sends. They do not send email or depend on the hosted editor. Review inbox compatibility and PDF pagination with your real content before deployment. See the [task guides](../../packages/react/docs/README.md) for constraints and adaptation. diff --git a/examples/content-workflows/package.json b/examples/content-workflows/package.json index 2f847f3e..adf14d00 100644 --- a/examples/content-workflows/package.json +++ b/examples/content-workflows/package.json @@ -13,7 +13,16 @@ "send:ses": "tsx src/providers/ses.ts", "send:postmark": "tsx src/providers/postmark.ts", "send:sendgrid": "tsx src/providers/sendgrid.ts", - "send:nodemailer": "tsx src/providers/nodemailer.ts" + "send:nodemailer": "tsx src/providers/nodemailer.ts", + "send:mailgun": "tsx src/providers/mailgun.ts", + "send:azure": "tsx src/providers/azure.ts", + "send:mailersend": "tsx src/providers/mailersend.ts", + "send:scaleway": "tsx src/providers/scaleway.ts", + "send:plunk": "tsx src/providers/plunk.ts", + "send:lettermint": "tsx src/providers/lettermint.ts", + "send:brevo": "tsx src/providers/brevo.ts", + "send:inbound": "tsx src/providers/inbound.ts", + "send:eusend": "tsx src/providers/eusend.ts" }, "dependencies": { "@unlayer/react-elements": "workspace:*", @@ -24,7 +33,16 @@ "@aws-sdk/client-sesv2": "^3.0.0", "postmark": "^4.0.0", "@sendgrid/mail": "^8.1.0", - "nodemailer": "^7.0.0" + "nodemailer": "^7.0.0", + "mailgun.js": "14.1.0", + "@azure/communication-email": "1.1.0", + "mailersend": "3.4.1", + "@scaleway/sdk-client": "2.14.0", + "@scaleway/sdk-tem": "2.16.1", + "lettermint": "3.1.0", + "@getbrevo/brevo": "6.1.0", + "inboundemail": "0.20.0", + "@eusend_dev/sdk": "0.19.0" }, "devDependencies": { "@types/node": "^24.9.1", @@ -33,6 +51,7 @@ "@types/nodemailer": "^7.0.0", "tsx": "^4.20.5", "typescript": "^5.6.3", - "http-server": "14.1.1" + "http-server": "14.1.1", + "nock": "14.0.17" } } diff --git a/examples/content-workflows/src/providers/azure.ts b/examples/content-workflows/src/providers/azure.ts new file mode 100644 index 00000000..dfb58769 --- /dev/null +++ b/examples/content-workflows/src/providers/azure.ts @@ -0,0 +1,16 @@ +import { EmailClient } from "@azure/communication-email"; +import { deliveryMessage, requireEnv } from "../message"; + +const client = new EmailClient(requireEnv("AZURE_COMMUNICATION_CONNECTION_STRING")); +const { from, to, subject, html, text } = deliveryMessage(); +// beginSend queues the message and returns a poller for the send operation. +const poller = await client.beginSend({ + senderAddress: from, + recipients: { to: [{ address: to }] }, + content: { subject, html, plainText: text }, +}); +const result = await poller.pollUntilDone(); +if (result.status !== "Succeeded") { + throw new Error(`Azure Communication Services: ${result.error?.message ?? result.status}`); +} +console.info("Azure accepted:", result.id); diff --git a/examples/content-workflows/src/providers/brevo.ts b/examples/content-workflows/src/providers/brevo.ts new file mode 100644 index 00000000..06dee5f7 --- /dev/null +++ b/examples/content-workflows/src/providers/brevo.ts @@ -0,0 +1,13 @@ +import { BrevoClient } from "@getbrevo/brevo"; +import { deliveryMessage, requireEnv } from "../message"; + +const client = new BrevoClient({ apiKey: requireEnv("BREVO_API_KEY") }); +const { from, to, subject, html, text } = deliveryMessage(); +const result = await client.transactionalEmails.sendTransacEmail({ + sender: { email: from }, + to: [{ email: to }], + subject, + htmlContent: html, + textContent: text, +}); +console.info("Brevo accepted:", result.messageId); diff --git a/examples/content-workflows/src/providers/eusend.ts b/examples/content-workflows/src/providers/eusend.ts new file mode 100644 index 00000000..16ea473d --- /dev/null +++ b/examples/content-workflows/src/providers/eusend.ts @@ -0,0 +1,7 @@ +import { Eusend } from "@eusend_dev/sdk"; +import { deliveryMessage, requireEnv } from "../message"; + +const client = new Eusend(requireEnv("EUSEND_API_KEY")); +const { data, error } = await client.emails.send(deliveryMessage()); +if (error) throw new Error(`eusend: ${error.name} ${error.message}`); +console.info("eusend accepted:", data.id); diff --git a/examples/content-workflows/src/providers/inbound.ts b/examples/content-workflows/src/providers/inbound.ts new file mode 100644 index 00000000..cd4dfa7d --- /dev/null +++ b/examples/content-workflows/src/providers/inbound.ts @@ -0,0 +1,7 @@ +import Inbound from "inboundemail"; +import { deliveryMessage, requireEnv } from "../message"; + +const client = new Inbound({ apiKey: requireEnv("INBOUND_API_KEY") }); +const { from, to, subject, html, text } = deliveryMessage(); +const result = await client.emails.send({ from, to, subject, html, text }); +console.info("Inbound accepted:", result.id); diff --git a/examples/content-workflows/src/providers/lettermint.ts b/examples/content-workflows/src/providers/lettermint.ts new file mode 100644 index 00000000..b41356c1 --- /dev/null +++ b/examples/content-workflows/src/providers/lettermint.ts @@ -0,0 +1,7 @@ +import { Lettermint } from "lettermint"; +import { deliveryMessage, requireEnv } from "../message"; + +const client = new Lettermint({ sendingToken: requireEnv("LETTERMINT_PROJECT_TOKEN") }); +const { from, to, subject, html, text } = deliveryMessage(); +const result = await client.emails.send({ from, to: [to], subject, html, text }); +console.info("Lettermint accepted:", result.message_id); diff --git a/examples/content-workflows/src/providers/mailersend.ts b/examples/content-workflows/src/providers/mailersend.ts new file mode 100644 index 00000000..305d4198 --- /dev/null +++ b/examples/content-workflows/src/providers/mailersend.ts @@ -0,0 +1,13 @@ +import { EmailParams, MailerSend, Recipient, Sender } from "mailersend"; +import { deliveryMessage, requireEnv } from "../message"; + +const client = new MailerSend({ apiKey: requireEnv("MAILERSEND_API_KEY") }); +const { from, to, subject, html, text } = deliveryMessage(); +const params = new EmailParams() + .setFrom(new Sender(from)) + .setTo([new Recipient(to)]) + .setSubject(subject) + .setHtml(html) + .setText(text); +const response = await client.email.send(params); +console.info("MailerSend accepted:", response.statusCode, response.headers["x-message-id"]); diff --git a/examples/content-workflows/src/providers/mailgun.ts b/examples/content-workflows/src/providers/mailgun.ts new file mode 100644 index 00000000..01dfab9b --- /dev/null +++ b/examples/content-workflows/src/providers/mailgun.ts @@ -0,0 +1,19 @@ +import Mailgun from "mailgun.js"; +import { deliveryMessage, requireEnv } from "../message"; + +// mailgun.js builds a multipart request with the FormData that Node.js 18+ provides. +const client = new Mailgun(FormData).client({ + username: "api", + key: requireEnv("MAILGUN_API_KEY"), + // Domains in Mailgun's EU region use https://api.eu.mailgun.net. + url: process.env.MAILGUN_URL ?? "https://api.mailgun.net", +}); +const { from, to, subject, html, text } = deliveryMessage(); +const result = await client.messages.create(requireEnv("MAILGUN_DOMAIN"), { + from, + to: [to], + subject, + html, + text, +}); +console.info("Mailgun accepted:", result.id); diff --git a/examples/content-workflows/src/providers/plunk.ts b/examples/content-workflows/src/providers/plunk.ts new file mode 100644 index 00000000..960bf7f4 --- /dev/null +++ b/examples/content-workflows/src/providers/plunk.ts @@ -0,0 +1,20 @@ +import { deliveryMessage, requireEnv } from "../message"; + +// The Plunk npm package is deprecated, so this calls Plunk's REST API directly. +// Plunk derives the plain-text part from `body`; the API has no text field. +type PlunkResponse = + | { success: true; data: { emails: { email: string }[] } } + | { success: false; error: { code: string; message: string } }; + +const { from, to, subject, html } = deliveryMessage(); +const response = await fetch("https://next-api.useplunk.com/v1/send", { + method: "POST", + headers: { + Authorization: `Bearer ${requireEnv("PLUNK_SECRET_KEY")}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ from, to, subject, body: html }), +}); +const result = (await response.json()) as PlunkResponse; +if (!result.success) throw new Error(`Plunk ${response.status}: ${result.error.message}`); +console.info("Plunk accepted:", result.data.emails[0].email); diff --git a/examples/content-workflows/src/providers/scaleway.ts b/examples/content-workflows/src/providers/scaleway.ts new file mode 100644 index 00000000..0d9ab30b --- /dev/null +++ b/examples/content-workflows/src/providers/scaleway.ts @@ -0,0 +1,21 @@ +import { createClient } from "@scaleway/sdk-client"; +import { Temv1alpha1 } from "@scaleway/sdk-tem"; +import { deliveryMessage, requireEnv } from "../message"; + +const client = createClient({ + accessKey: requireEnv("SCW_ACCESS_KEY"), + secretKey: requireEnv("SCW_SECRET_KEY"), + defaultProjectId: requireEnv("SCW_DEFAULT_PROJECT_ID"), + // Transactional Email runs in the fr-par region. + defaultRegion: "fr-par", +}); +const tem = new Temv1alpha1.API(client); +const { from, to, subject, html, text } = deliveryMessage(); +const { emails } = await tem.createEmail({ + from: { email: from }, + to: [{ email: to }], + subject, + html, + text, +}); +console.info("Scaleway accepted:", emails.map((email) => email.id).join(", ")); diff --git a/examples/content-workflows/src/workflows.test.ts b/examples/content-workflows/src/workflows.test.ts index da0ab534..43571b6a 100644 --- a/examples/content-workflows/src/workflows.test.ts +++ b/examples/content-workflows/src/workflows.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { test } from "node:test"; import { renderToHtml, renderToJson } from "@unlayer/react-elements"; +import nock from "nock"; import { chromium } from "playwright"; import { invoiceTemplate, sampleInvoice } from "./invoice"; import { renderInvoiceEmail } from "./message"; @@ -54,3 +55,184 @@ test("document HTML produces a real PDF", async () => { assert.equal(pdf.subarray(0, 5).toString(), "%PDF-"); assert.ok(pdf.length > 1000); }); + +// The provider scripts run as written; nock answers their HTTP requests, so nothing is sent. +type SentRequest = { headers: Record; body: nock.Body }; +type SentMessage = { from?: string; to?: string; subject?: string; html?: string; text?: string }; + +function record(interceptor: nock.Interceptor, status: number, response: nock.ReplyBody, headers = {}) { + const sent: SentRequest[] = []; + interceptor.reply(function (_uri, body) { + sent.push({ headers: this.req.headers, body }); + return [status, response, { "content-type": "application/json", ...headers }]; + }); + return sent; +} + +function json(body: nock.Body): Record { + return typeof body === "string" ? JSON.parse(body) : body; +} + +// multipart/form-data sends line breaks in field values as CRLF. +function multipartFields(body: nock.Body): Record { + const fields: Record = {}; + for (const [, name, value] of String(body).matchAll(/name="([^"]+)"\r\n(?:[^\r\n]+\r\n)*\r\n([\s\S]*?)\r\n--/g)) { + fields[name] = value.replace(/\r\n/g, "\n"); + } + return fields; +} + +const sender = "receipts@example.com"; +const recipient = "ada@example.com"; +const azureOrigin = "https://example-resource.communication.azure.com"; + +const providers: { + script: string; + env: Record; + mock: () => SentRequest[]; + authorized: (headers: Record) => boolean; + message: (body: nock.Body) => SentMessage; + printed: string; +}[] = [ + { + script: "mailgun", + env: { MAILGUN_API_KEY: "test-key", MAILGUN_DOMAIN: "mg.example.com" }, + mock: () => record(nock("https://api.mailgun.net").post("/v3/mg.example.com/messages"), 200, { id: "", message: "Queued. Thank you." }), + authorized: (headers) => headers.authorization === `Basic ${Buffer.from("api:test-key").toString("base64")}`, + message: (body) => { + const { from, to, subject, html, text } = multipartFields(body); + return { from, to, subject, html, text }; + }, + printed: "Mailgun accepted: ", + }, + { + script: "azure", + env: { AZURE_COMMUNICATION_CONNECTION_STRING: `endpoint=${azureOrigin}/;accesskey=${Buffer.from("test-key").toString("base64")}` }, + mock: () => { + nock(azureOrigin).get("/emails/operations/azure-1").query(true).reply(200, { id: "azure-1", status: "Succeeded" }); + return record(nock(azureOrigin).post("/emails:send").query(true), 202, { id: "azure-1", status: "Running" }, { + "operation-location": `${azureOrigin}/emails/operations/azure-1?api-version=2025-09-01`, + }); + }, + authorized: (headers) => headers.authorization.startsWith("HMAC-SHA256 "), + message: (body) => { + const { senderAddress, recipients, content } = json(body); + return { from: senderAddress, to: recipients.to[0].address, subject: content.subject, html: content.html, text: content.plainText }; + }, + printed: "Azure accepted: azure-1", + }, + { + script: "mailersend", + env: { MAILERSEND_API_KEY: "test-key" }, + mock: () => record(nock("https://api.mailersend.com").post("/v1/email"), 202, "", { "x-message-id": "mailersend-1" }), + authorized: (headers) => headers.authorization === "Bearer test-key", + message: (body) => { + const { from, to, subject, html, text } = json(body); + return { from: from.email, to: to[0].email, subject, html, text }; + }, + printed: "MailerSend accepted: 202 mailersend-1", + }, + { + script: "scaleway", + env: { + SCW_ACCESS_KEY: "SCWXXXXXXXXXXXXXXXXX", + SCW_SECRET_KEY: "11111111-1111-1111-1111-111111111111", + SCW_DEFAULT_PROJECT_ID: "22222222-2222-2222-2222-222222222222", + }, + mock: () => record(nock("https://api.scaleway.com").post("/transactional-email/v1alpha1/regions/fr-par/emails"), 200, { + emails: [{ id: "scaleway-1", message_id: "message-1", project_id: "22222222-2222-2222-2222-222222222222", mail_from: sender, mail_rcpt: recipient, rcpt_type: "to", subject: "Receipt", status: "new", try_count: 0, flags: [], last_tries: [] }], + }), + authorized: (headers) => headers["x-auth-token"] === "11111111-1111-1111-1111-111111111111", + message: (body) => { + const { from, to, subject, html, text, project_id } = json(body); + assert.equal(project_id, "22222222-2222-2222-2222-222222222222"); + return { from: from.email, to: to[0].email, subject, html, text }; + }, + printed: "Scaleway accepted: scaleway-1", + }, + { + script: "plunk", + env: { PLUNK_SECRET_KEY: "sk_test" }, + mock: () => record(nock("https://next-api.useplunk.com").post("/v1/send"), 200, { + success: true, + data: { emails: [{ contact: { id: "contact-1", email: recipient }, email: "plunk-1" }], timestamp: "2026-01-01T00:00:00.000Z" }, + }), + authorized: (headers) => headers.authorization === "Bearer sk_test", + // Plunk takes no plain-text part: it derives one from the HTML. + message: (body) => { + const { from, to, subject, body: html } = json(body); + return { from, to, subject, html }; + }, + printed: "Plunk accepted: plunk-1", + }, + { + script: "lettermint", + env: { LETTERMINT_PROJECT_TOKEN: "lm_test" }, + mock: () => record(nock("https://api.lettermint.co").post("/v1/send"), 202, { message_id: "lettermint-1", status: "pending" }), + authorized: (headers) => headers["x-lettermint-token"] === "lm_test", + message: (body) => { + const { from, to, subject, html, text } = json(body); + return { from, to: to[0], subject, html, text }; + }, + printed: "Lettermint accepted: lettermint-1", + }, + { + script: "brevo", + env: { BREVO_API_KEY: "test-key" }, + mock: () => record(nock("https://api.brevo.com").post("/v3/smtp/email"), 201, { messageId: "" }), + authorized: (headers) => headers["api-key"] === "test-key", + message: (body) => { + const { sender: from, to, subject, htmlContent, textContent } = json(body); + return { from: from.email, to: to[0].email, subject, html: htmlContent, text: textContent }; + }, + printed: "Brevo accepted: ", + }, + { + script: "inbound", + env: { INBOUND_API_KEY: "test-key" }, + mock: () => record(nock("https://inbound.new").post("/api/e2/emails"), 200, { id: "inbound-1" }), + authorized: (headers) => headers.authorization === "Bearer test-key", + message: (body) => { + const { from, to, subject, html, text } = json(body); + return { from, to, subject, html, text }; + }, + printed: "Inbound accepted: inbound-1", + }, + { + script: "eusend", + env: { EUSEND_API_KEY: "eu_test_key" }, + mock: () => record(nock("https://api.eusend.dev").post("/emails"), 200, { id: "eusend-1" }), + authorized: (headers) => headers.authorization === "Bearer eu_test_key", + message: (body) => { + const { from, to, subject, html, text } = json(body); + return { from, to, subject, html, text }; + }, + printed: "eusend accepted: eusend-1", + }, +]; + +test("each provider script sends the rendered HTML and plain text", async (t) => { + const { subject, html, text } = renderInvoiceEmail(); + nock.disableNetConnect(); + try { + for (const provider of providers) { + await t.test(provider.script, async (st) => { + Object.assign(process.env, { EMAIL_FROM: sender, EMAIL_TO: recipient }, provider.env); + const sent = provider.mock(); + const info = st.mock.method(console, "info", () => {}); + await import(`./providers/${provider.script}`); + assert.deepEqual(nock.pendingMocks(), []); + assert.equal(sent.length, 1); + assert.ok(provider.authorized(sent[0].headers), "the request carries the credential"); + const message = provider.message(sent[0].body); + const expected: SentMessage = { from: sender, to: recipient, subject, html, text }; + if (!("text" in message)) delete expected.text; + assert.deepEqual(message, expected); + assert.equal(info.mock.calls.map((call) => call.arguments.join(" ")).join("\n"), provider.printed); + }); + } + } finally { + nock.cleanAll(); + nock.enableNetConnect(); + } +}); diff --git a/examples/i18n/.gitignore b/examples/i18n/.gitignore new file mode 100644 index 00000000..ea1472ec --- /dev/null +++ b/examples/i18n/.gitignore @@ -0,0 +1 @@ +output/ diff --git a/examples/i18n/README.md b/examples/i18n/README.md new file mode 100644 index 00000000..cfa43ca8 --- /dev/null +++ b/examples/i18n/README.md @@ -0,0 +1,25 @@ +# Translated React emails: react-intl, react-i18next, and next-intl + +One shipping notification rendered with `@unlayer/react-elements` in English and Arabic (right-to-left), once with each of three i18n libraries. The [i18n guide](../../packages/react/docs/i18n.md) walks through the code. Commands run from the repository root with Node.js 20+ and pnpm 9. + +```bash +pnpm install +pnpm build +pnpm --filter @unlayer/i18n-example test +pnpm --filter @unlayer/i18n-example render +``` + +`render` writes `output/shipped.en.html`, `output/shipped.ar.html`, and the plain text of each, using the react-intl version. + +## Files + +- `messages/en.json`, `messages/ar.json`: the messages, in ICU syntax, shared by all three libraries. +- `src/locales.ts`: loads a locale's messages (before rendering) and maps it to a text direction. +- `src/react-intl.tsx`: `createIntl()` + `RawIntlProvider`, `useIntl()` in the template. +- `src/react-i18next.tsx`: an initialized i18next instance per locale, `I18nextProvider`, `useTranslation()`. +- `src/next-intl.tsx`: `createTranslator()`, which needs no Next.js request. +- `src/i18n.test.ts`: renders both locales with each library and checks the subject, the translated text in the HTML and plain text, `lang`, `dir`, and the text alignment; and that rendering before i18next has loaded its messages fails. + +```bash +pnpm --filter @unlayer/i18n-example typecheck +``` diff --git a/examples/i18n/messages/ar.json b/examples/i18n/messages/ar.json new file mode 100644 index 00000000..aef017df --- /dev/null +++ b/examples/i18n/messages/ar.json @@ -0,0 +1,9 @@ +{ + "shipped": { + "subject": "تم شحن طلبك {orderId}", + "preview": "طلبك في الطريق إليك.", + "heading": "تم شحن طلبك", + "greeting": "مرحبًا {name}، طلبك {orderId} في الطريق إليك.", + "cta": "تتبّع شحنتك" + } +} diff --git a/examples/i18n/messages/en.json b/examples/i18n/messages/en.json new file mode 100644 index 00000000..1806a5c7 --- /dev/null +++ b/examples/i18n/messages/en.json @@ -0,0 +1,9 @@ +{ + "shipped": { + "subject": "Order {orderId} has shipped", + "preview": "Your order is on its way.", + "heading": "Your order has shipped", + "greeting": "Hi {name}, order {orderId} is on its way.", + "cta": "Track your package" + } +} diff --git a/examples/i18n/package.json b/examples/i18n/package.json new file mode 100644 index 00000000..46878b0c --- /dev/null +++ b/examples/i18n/package.json @@ -0,0 +1,28 @@ +{ + "name": "@unlayer/i18n-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "render": "tsx src/render.ts", + "typecheck": "tsc --noEmit", + "test": "tsx --test src/i18n.test.ts" + }, + "dependencies": { + "@unlayer/react-elements": "workspace:*", + "i18next": "26.4.4", + "next": "16.3.8", + "next-intl": "4.14.9", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-i18next": "17.0.16", + "react-intl": "12.1.4" + }, + "devDependencies": { + "@types/node": "24.9.1", + "@types/react": "18.3.26", + "@types/react-dom": "18.3.7", + "tsx": "4.21.0", + "typescript": "5.9.3" + } +} diff --git a/examples/i18n/src/i18n.test.ts b/examples/i18n/src/i18n.test.ts new file mode 100644 index 00000000..332d4df5 --- /dev/null +++ b/examples/i18n/src/i18n.test.ts @@ -0,0 +1,59 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import i18next from "i18next"; +import { loadMessages, sampleOrder, type Locale } from "./locales"; +import * as withNextIntl from "./next-intl"; +import { createI18n, renderShippedEmail as renderWithI18next } from "./react-i18next"; +import * as withReactIntl from "./react-intl"; + +const expected: Record = { + en: { + dir: "ltr", + subject: "Order A-1042 has shipped", + heading: "Your order has shipped", + greeting: "Hi Ada, order A-1042 is on its way.", + cta: "Track your package", + preview: "Your order is on its way.", + }, + ar: { + dir: "rtl", + subject: "تم شحن طلبك A-1042", + heading: "تم شحن طلبك", + greeting: "مرحبًا Ada، طلبك A-1042 في الطريق إليك.", + cta: "تتبّع شحنتك", + preview: "طلبك في الطريق إليك.", + }, +}; + +const renderers: Record Promise<{ subject: string; html: string; text: string }>> = { + "react-intl": async (locale) => withReactIntl.renderShippedEmail(locale, await loadMessages(locale), sampleOrder), + "react-i18next": async (locale) => renderWithI18next(await createI18n(locale, await loadMessages(locale)), sampleOrder), + "next-intl": async (locale) => withNextIntl.renderShippedEmail(locale, await loadMessages(locale), sampleOrder), +}; + +for (const [library, render] of Object.entries(renderers)) { + test(`${library} renders the email in English and Arabic`, async () => { + for (const locale of ["en", "ar"] as const) { + const want = expected[locale]; + const { subject, html, text } = await render(locale); + const htmlTag = html.match(/]*>/)?.[0] ?? ""; + assert.match(htmlTag, new RegExp(` lang="${locale}"`)); + assert.match(htmlTag, new RegExp(` dir="${want.dir}"`)); + assert.equal(subject, want.subject); + for (const phrase of [want.heading, want.greeting, want.cta, want.preview]) { + assert.ok(html.includes(phrase), `${locale} HTML contains "${phrase}"`); + } + assert.ok(text.includes(want.greeting), `${locale} plain text contains the greeting`); + // The heading and paragraph start on the reading side: right in Arabic, left in English. + const aligned = html.match(/text-align: (left|right)/g) ?? []; + assert.deepEqual(aligned, [`text-align: ${want.dir === "rtl" ? "right" : "left"}`, `text-align: ${want.dir === "rtl" ? "right" : "left"}`]); + } + }); +} + +test("rendering before i18next has loaded the messages fails instead of sending an empty email", (t) => { + // react-i18next warns that useTranslation suspended; Elements then refuses to render. + t.mock.method(console, "warn", () => {}); + const notLoaded = i18next.createInstance(); + assert.throws(() => renderWithI18next(notLoaded, sampleOrder), /async and suspending templates need/); +}); diff --git a/examples/i18n/src/locales.ts b/examples/i18n/src/locales.ts new file mode 100644 index 00000000..79e77ddc --- /dev/null +++ b/examples/i18n/src/locales.ts @@ -0,0 +1,33 @@ +import { readFile } from "node:fs/promises"; + +export const locales = ["en", "ar"] as const; +export type Locale = (typeof locales)[number]; + +/** One file per locale in messages/, nested by email, with ICU placeholders: "Hi {name}". */ +export interface Messages { + shipped: Record<"subject" | "preview" | "heading" | "greeting" | "cta", string>; +} + +/** Elements renders synchronously, so load a locale's messages before rendering. */ +export async function loadMessages(locale: Locale): Promise { + return JSON.parse(await readFile(new URL(`../messages/${locale}.json`, import.meta.url), "utf8")); +} + +const RTL_LANGUAGES = new Set(["ar", "fa", "he", "ur"]); + +/** The value for the root's `textDirection` prop. */ +export function directionOf(locale: string): "ltr" | "rtl" { + return RTL_LANGUAGES.has(new Intl.Locale(locale).language) ? "rtl" : "ltr"; +} + +export interface Order { + id: string; + customer: string; + trackingUrl: string; +} + +export const sampleOrder: Order = { + id: "A-1042", + customer: "Ada", + trackingUrl: "https://example.com/orders/A-1042", +}; diff --git a/examples/i18n/src/next-intl.tsx b/examples/i18n/src/next-intl.tsx new file mode 100644 index 00000000..2be1114d --- /dev/null +++ b/examples/i18n/src/next-intl.tsx @@ -0,0 +1,34 @@ +import { + Button, Column, ColumnLayouts, Email, Heading, Paragraph, Row, + renderToHtml, renderToPlainText, +} from "@unlayer/react-elements"; +import { createTranslator } from "next-intl"; +import { directionOf, type Locale, type Messages, type Order } from "./locales"; + +type ShippedTranslator = ReturnType>; + +function shippedEmail(t: ShippedTranslator, locale: Locale, order: Order) { + const direction = directionOf(locale); + return ( + + + + {t("heading")} + {t("greeting", { name: order.customer, orderId: order.id })} + + + + + ); +} + +export function renderShippedEmail(locale: Locale, messages: Messages, order: Order) { + // createTranslator needs no request: it works in scripts, job workers and Next.js server code alike. + const t = createTranslator({ locale, messages, namespace: "shipped" }); + const email = shippedEmail(t, locale, order); + return { + subject: t("subject", { orderId: order.id }), + html: renderToHtml(email), + text: renderToPlainText(email), + }; +} diff --git a/examples/i18n/src/react-i18next.tsx b/examples/i18n/src/react-i18next.tsx new file mode 100644 index 00000000..b28bb91f --- /dev/null +++ b/examples/i18n/src/react-i18next.tsx @@ -0,0 +1,53 @@ +import { + Button, Column, ColumnLayouts, Email, Heading, Paragraph, Row, + renderToHtml, renderToPlainText, +} from "@unlayer/react-elements"; +import i18next, { type i18n } from "i18next"; +import { I18nextProvider, useTranslation } from "react-i18next"; +import type { Locale, Messages, Order } from "./locales"; + +/** An i18next instance per locale, initialized before anything renders with it. */ +export async function createI18n(locale: Locale, messages: Messages): Promise { + const instance = i18next.createInstance(); + await instance.init({ + lng: locale, + resources: { [locale]: { translation: messages } }, + interpolation: { + // The shared message files use {name}; i18next's default is {{name}}. + prefix: "{", + suffix: "}", + // Elements escapes text children already. + escapeValue: false, + }, + }); + return instance; +} + +function ShippedEmail({ order }: { order: Order }) { + const { t, i18n } = useTranslation(); + const direction = i18n.dir(); + return ( + + + + {t("shipped.heading")} + {t("shipped.greeting", { name: order.customer, orderId: order.id })} + + + + + ); +} + +export function renderShippedEmail(i18n: i18n, order: Order) { + const email = ( + + + + ); + return { + subject: i18n.t("shipped.subject", { orderId: order.id }), + html: renderToHtml(email), + text: renderToPlainText(email), + }; +} diff --git a/examples/i18n/src/react-intl.tsx b/examples/i18n/src/react-intl.tsx new file mode 100644 index 00000000..84f4c556 --- /dev/null +++ b/examples/i18n/src/react-intl.tsx @@ -0,0 +1,45 @@ +import { + Button, Column, ColumnLayouts, Email, Heading, Paragraph, Row, + renderToHtml, renderToPlainText, +} from "@unlayer/react-elements"; +import { createIntl, RawIntlProvider, useIntl } from "react-intl"; +import { directionOf, type Locale, type Messages, type Order } from "./locales"; + +function ShippedEmail({ order }: { order: Order }) { + const intl = useIntl(); + const values = { name: order.customer, orderId: order.id }; + const direction = directionOf(intl.locale); + return ( + + + + {intl.formatMessage({ id: "shipped.heading" })} + {intl.formatMessage({ id: "shipped.greeting" }, values)} + + + + + ); +} + +export function renderShippedEmail(locale: Locale, messages: Messages, order: Order) { + // react-intl looks messages up by flat ids: { "shipped.heading": "…" }. + const flat = Object.fromEntries( + Object.entries(messages.shipped).map(([key, message]) => [`shipped.${key}`, message]), + ); + const intl = createIntl({ locale, messages: flat }); + const email = ( + + + + ); + return { + subject: intl.formatMessage({ id: "shipped.subject" }, { orderId: order.id }), + html: renderToHtml(email), + text: renderToPlainText(email), + }; +} diff --git a/examples/i18n/src/render.ts b/examples/i18n/src/render.ts new file mode 100644 index 00000000..cd6584f5 --- /dev/null +++ b/examples/i18n/src/render.ts @@ -0,0 +1,11 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { loadMessages, locales, sampleOrder } from "./locales"; +import { renderShippedEmail } from "./react-intl"; + +await mkdir("output", { recursive: true }); +for (const locale of locales) { + const { subject, html, text } = renderShippedEmail(locale, await loadMessages(locale), sampleOrder); + await writeFile(`output/shipped.${locale}.html`, html); + await writeFile(`output/shipped.${locale}.txt`, text); + console.info(`${locale}: ${subject} -> output/shipped.${locale}.html`); +} diff --git a/examples/i18n/tsconfig.json b/examples/i18n/tsconfig.json new file mode 100644 index 00000000..2e1f3423 --- /dev/null +++ b/examples/i18n/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "types": ["node", "react", "react-dom"] + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c325dd23..a1c319e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: version: 27.0.0 tsup: specifier: ^8.0.1 - version: 8.5.0(@swc/core@1.15.40)(jiti@2.6.1)(postcss@8.5.30)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(@swc/core@1.16.18(@swc/helpers@0.5.23))(jiti@2.6.1)(postcss@8.5.30)(tsx@4.21.0)(typescript@5.9.3) typescript: specifier: ^5.6.3 version: 5.9.3 @@ -32,12 +32,39 @@ importers: '@aws-sdk/client-sesv2': specifier: ^3.0.0 version: 3.1146.0 + '@azure/communication-email': + specifier: 1.1.0 + version: 1.1.0 + '@eusend_dev/sdk': + specifier: 0.19.0 + version: 0.19.0(@react-email/render@2.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@getbrevo/brevo': + specifier: 6.1.0 + version: 6.1.0 + '@scaleway/sdk-client': + specifier: 2.14.0 + version: 2.14.0 + '@scaleway/sdk-tem': + specifier: 2.16.1 + version: 2.16.1(@scaleway/sdk-client@2.14.0) '@sendgrid/mail': specifier: ^8.1.0 version: 8.1.6 '@unlayer/react-elements': specifier: workspace:* version: link:../../packages/react + inboundemail: + specifier: 0.20.0 + version: 0.20.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + lettermint: + specifier: 3.1.0 + version: 3.1.0 + mailersend: + specifier: 3.4.1 + version: 3.4.1 + mailgun.js: + specifier: 14.1.0 + version: 14.1.0 nodemailer: specifier: ^7.0.0 version: 7.0.13 @@ -72,6 +99,9 @@ importers: http-server: specifier: 14.1.1 version: 14.1.1 + nock: + specifier: 14.0.17 + version: 14.0.17 tsx: specifier: ^4.20.5 version: 4.21.0 @@ -79,6 +109,49 @@ importers: specifier: ^5.6.3 version: 5.9.3 + examples/i18n: + dependencies: + '@unlayer/react-elements': + specifier: workspace:* + version: link:../../packages/react + i18next: + specifier: 26.4.4 + version: 26.4.4(typescript@5.9.3) + next: + specifier: 16.3.8 + version: 16.3.8(@types/node@24.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.93.2) + next-intl: + specifier: 4.14.9 + version: 4.14.9(@swc/helpers@0.5.23)(@types/react@18.3.26)(next@16.3.8(@types/node@24.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.93.2))(react@18.3.1) + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + react-i18next: + specifier: 17.0.16 + version: 17.0.16(i18next@26.4.4(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + react-intl: + specifier: 12.1.4 + version: 12.1.4(@types/react@18.3.26)(react@18.3.1) + devDependencies: + '@types/node': + specifier: 24.9.1 + version: 24.9.1 + '@types/react': + specifier: 18.3.26 + version: 18.3.26 + '@types/react-dom': + specifier: 18.3.7 + version: 18.3.7(@types/react@18.3.26) + tsx: + specifier: 4.21.0 + version: 4.21.0 + typescript: + specifier: 5.9.3 + version: 5.9.3 + examples/react-email-in-editor: dependencies: '@react-email/components': @@ -228,7 +301,7 @@ importers: version: 18.3.1(react@18.3.1) tsup: specifier: ^8.0.1 - version: 8.5.0(@swc/core@1.15.40)(jiti@2.6.1)(postcss@8.5.30)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(@swc/core@1.16.18(@swc/helpers@0.5.23))(jiti@2.6.1)(postcss@8.5.30)(tsx@4.21.0)(typescript@5.9.3) tsx: specifier: ^4.20.5 version: 4.21.0 @@ -286,7 +359,7 @@ importers: version: react@19.1.0 tsup: specifier: ^8.0.1 - version: 8.5.0(@swc/core@1.15.40)(jiti@2.6.1)(postcss@8.5.30)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(@swc/core@1.16.18(@swc/helpers@0.5.23))(jiti@2.6.1)(postcss@8.5.30)(tsx@4.21.0)(typescript@5.9.3) vitest: specifier: ^3.2.4 version: 3.2.4(@types/node@24.9.1)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@24.1.3)(less@4.4.2)(lightningcss@1.31.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.21.0) @@ -317,7 +390,7 @@ importers: version: 9.1.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.52.5)(storybook@9.1.16(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.31.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.21.0)))(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.31.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.21.0)) '@storybook/test-runner': specifier: 0.23.0 - version: 0.23.0(@types/node@24.9.1)(storybook@9.1.16(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.31.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.21.0))) + version: 0.23.0(@swc/helpers@0.5.23)(@types/node@24.9.1)(storybook@9.1.16(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.31.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.21.0))) '@testing-library/jest-dom': specifier: ^6.4.2 version: 6.9.1 @@ -374,7 +447,7 @@ importers: version: 4.2.1 tsup: specifier: ^8.0.1 - version: 8.5.0(@swc/core@1.15.40)(jiti@2.6.1)(postcss@8.5.30)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(@swc/core@1.15.40(@swc/helpers@0.5.23))(jiti@2.6.1)(postcss@8.5.30)(tsx@4.21.0)(typescript@5.9.3) vitest: specifier: ^3.2.4 version: 3.2.4(@types/node@24.9.1)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@24.1.3)(less@4.4.2)(lightningcss@1.31.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.21.0) @@ -390,7 +463,7 @@ importers: devDependencies: tsup: specifier: ^8.0.1 - version: 8.5.0(@swc/core@1.15.40)(jiti@2.6.1)(postcss@8.5.30)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(@swc/core@1.16.18(@swc/helpers@0.5.23))(jiti@2.6.1)(postcss@8.5.30)(tsx@4.21.0)(typescript@5.9.3) vitest: specifier: ^3.2.4 version: 3.2.4(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@24.1.3)(less@4.4.2)(lightningcss@1.31.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.21.0) @@ -471,6 +544,50 @@ packages: resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} engines: {node: '>=18.0.0'} + '@azure-rest/core-client@2.9.0': + resolution: {integrity: sha512-6933vNLqh06RR7rumnrq3UZIeBtRLeBHDPMrieDPIljiCaMMwt0nRw++nd3TOxiohtasNtm29rC+b/9ZVKCOqg==} + engines: {node: '>=22.0.0'} + + '@azure/abort-controller@2.2.0': + resolution: {integrity: sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==} + engines: {node: '>=22.0.0'} + + '@azure/communication-common@2.5.0': + resolution: {integrity: sha512-6Z5OSOk8tWlOAZHAxgHGfLt8V755BHOvMks/7xLx2auQLmS+DJObgtq+1yE9gVYLu66tzpX/J779H8D9HDVNAQ==} + engines: {node: '>=22.0.0'} + + '@azure/communication-email@1.1.0': + resolution: {integrity: sha512-n9ATpXyxb4MIhEp/Vtv5BU8GdUZH0vYpm/1pKXVu9AeiQ78MG3OIaiQQ2PmQfA0XPdTx5/g+tUxkwVuD6U4u4w==} + engines: {node: '>=20.0.0'} + + '@azure/core-auth@1.11.0': + resolution: {integrity: sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==} + engines: {node: '>=22.0.0'} + + '@azure/core-client@1.11.1': + resolution: {integrity: sha512-2QygG2F76ZpMP2eMztiJvAiFMu71M9rDeU7vO/QKg5Css7MgM4frUOslFjhVjRhbGaCNPtz/S8M6y46/fFKVuQ==} + engines: {node: '>=22.0.0'} + + '@azure/core-lro@2.7.2': + resolution: {integrity: sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==} + engines: {node: '>=18.0.0'} + + '@azure/core-rest-pipeline@1.25.0': + resolution: {integrity: sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==} + engines: {node: '>=22.0.0'} + + '@azure/core-tracing@1.4.0': + resolution: {integrity: sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==} + engines: {node: '>=22.0.0'} + + '@azure/core-util@1.14.0': + resolution: {integrity: sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==} + engines: {node: '>=22.0.0'} + + '@azure/logger@1.4.0': + resolution: {integrity: sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==} + engines: {node: '>=22.0.0'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -649,6 +766,10 @@ packages: resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.10': + resolution: {integrity: sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -699,6 +820,18 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@eloqnt/config@0.1.1': + resolution: {integrity: sha512-YRDigSaiJEzKDfzTth5kG+L/PKfB3mo8h7yNzpXExPfvQmcD05WoZT57PdhJE0jI+Vva6eb0OCU8hnHHgdME3g==} + + '@eloqnt/format-json@0.1.0': + resolution: {integrity: sha512-4Pkb+HyzgWNJXoU48aJhUyNZSjZvMGooPCNG1ZFmBm55xTiNNPQCNXH66Ob6sXS6RvbWZrRpCf+Uo/Jz9K9g+A==} + + '@eloqnt/format-po@0.1.1': + resolution: {integrity: sha512-aLtmIoqXGEaeTT0RcsLN8sZ3aIrsT/c/dVs28zrLhDjO3+vA3KNP+r98N0xq8w/k36+/GIUt4uZNbqcA71GVWw==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@esbuild/aix-ppc64@0.25.11': resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} engines: {node: '>=18'} @@ -1011,6 +1144,43 @@ packages: cpu: [x64] os: [win32] + '@eusend_dev/sdk@0.19.0': + resolution: {integrity: sha512-b0jMDybpDuNeoTShYjJrdp0STdpJpahXp/9qUfw9uoUKA8lFr4u/tWbXaS/ti62O7u3bdCTXypMMDfXRmF6VVA==} + engines: {node: '>=18'} + peerDependencies: + '@react-email/render': '>=1.0.0' + react: '>=18.0.0' + peerDependenciesMeta: + '@react-email/render': + optional: true + react: + optional: true + + '@formatjs/fast-memoize@3.1.7': + resolution: {integrity: sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==} + + '@formatjs/icu-messageformat-parser@3.5.18': + resolution: {integrity: sha512-wX1efcL8d7K5QapOTRsP/OLTA/kpwe4LwqWyNxDNg5iRs8i9jJbCyxB5P8jc4KOW5oiLzz2DJr+BRY5xMV9+bA==} + + '@formatjs/icu-messageformat-parser@3.5.21': + resolution: {integrity: sha512-Vnl7IOSyMRaP4Pjv48etkT2nlcIwzh4QWIa3hewzGlZZPBbft+TbMir4MijpDViOLN/1hvkGC10oYMuilsG+fw==} + + '@formatjs/icu-skeleton-parser@2.1.11': + resolution: {integrity: sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==} + + '@formatjs/icu-skeleton-parser@2.1.12': + resolution: {integrity: sha512-ca7tZMLpfHX3sUD7hP6gE/qr6Iq89EDd3ZLeRhbsB7r0UD55DGJq+tmFLzrGrnx5StahUevQmQQVtbDRHsfDHA==} + + '@formatjs/intl-localematcher@0.8.14': + resolution: {integrity: sha512-8O7V38QmbB11+ryJ3KM/znrzXtiqgpXzqNoqDsndHZZ5/zDKTTL+cf5F0mzXkQEwhx8IlWnqYvEovf2S5EwLTw==} + + '@formatjs/intl@6.1.3': + resolution: {integrity: sha512-fBxQVx6wi0f3iibauVFKr/uY+BJR0Z64Zuu6SB+p8hmxjqZoX2u4/RPYaKnFMut2+kNPIeGOaaKDHQ5odnXsZQ==} + + '@getbrevo/brevo@6.1.0': + resolution: {integrity: sha512-QLJLqxI0A6+umc1hqZ0TY5bvrAJxNkjSCfzLA56/o5gyiJvaMwYzoFG6V21ajzVA0gKFEtyYjGjzrR2kx+/dvQ==} + engines: {node: '>=18.0.0'} + '@hapi/address@5.1.1': resolution: {integrity: sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==} engines: {node: '>=14.0.0'} @@ -1037,6 +1207,152 @@ packages: '@hapi/topo@6.0.2': resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1195,9 +1511,73 @@ packages: '@types/react': '>=16' react: '>=16' + '@mswjs/interceptors@0.41.9': + resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} + engines: {node: '>=18'} + + '@next/env@16.3.8': + resolution: {integrity: sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw==} + + '@next/swc-darwin-arm64@16.3.8': + resolution: {integrity: sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.3.8': + resolution: {integrity: sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.3.8': + resolution: {integrity: sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@16.3.8': + resolution: {integrity: sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@16.3.8': + resolution: {integrity: sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@16.3.8': + resolution: {integrity: sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@16.3.8': + resolution: {integrity: sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.3.8': + resolution: {integrity: sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@one-ini/wasm@0.2.1': resolution: {integrity: sha512-TUqERXGNTifZ9y2g3wPxQrw3HpHv/02DsW3D90T9x0hhonrL1ZqpSmNrU2XkoIq0fP1N6gZfVQzy2Fw1ZvGBNg==} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -1588,6 +1968,26 @@ packages: cpu: [x64] os: [win32] + '@scaleway/random-name@6.0.2': + resolution: {integrity: sha512-fhR2aCzxoZlOZcCM42srbALdpdqMy79LwOK5mVf4sGG1uknn095mr5PQ/0/zwfk/FX2KycZwbKJzr4zP6CG3Zg==} + engines: {node: '>=24.x'} + + '@scaleway/sdk-client@2.14.0': + resolution: {integrity: sha512-w7gWquZIdA4DoU6dA3RwMxO6r1Yk+6dG55bQwac2GjQDZ1azSh8zXjh5rkt0dPpcaONzibffYZ42x3voNLk45A==} + + '@scaleway/sdk-std@2.9.3': + resolution: {integrity: sha512-8B6BcgOI9pnZjJtNbHd5Gdpxw/jKMEqZ7NubjI0CnrbY361/b/OhuJ1JmsBpr+DrSx+kUD2103PM/KGetFLijw==} + peerDependencies: + '@scaleway/sdk-client': 2.14.0 + + '@scaleway/sdk-tem@2.16.1': + resolution: {integrity: sha512-fQxqsH7d8vTkgXv+VrSEhq2YCDGQRtkaOqh1eYKqc+eKbtUxdryEUhUt3AjQsGEAhmlunGpAn+kQEAeUn9rsMA==} + peerDependencies: + '@scaleway/sdk-client': ^2.14.0 + + '@schummar/icu-type-parser@1.21.5': + resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==} + '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} @@ -1726,72 +2126,144 @@ packages: cpu: [arm64] os: [darwin] + '@swc/core-darwin-arm64@1.16.18': + resolution: {integrity: sha512-ozgiCHyFDCwfXzTnaLR+i8WxEyt06GTudn9rPh08vj+u3Z8vMvt5Pi8T+BikyNXm7y++h6UMdqsnJuWrV+tk2A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + '@swc/core-darwin-x64@1.15.40': resolution: {integrity: sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==} engines: {node: '>=10'} cpu: [x64] os: [darwin] + '@swc/core-darwin-x64@1.16.18': + resolution: {integrity: sha512-tuwxXi+sjxxTAi7GP3NeQXQxOf4SXSiFxKAMYLBdoy7JqGYby6uSDXojcFoLOhSp7oiNGstnaFXxfbP0Hpro9w==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + '@swc/core-linux-arm-gnueabihf@1.15.40': resolution: {integrity: sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==} engines: {node: '>=10'} cpu: [arm] os: [linux] + '@swc/core-linux-arm-gnueabihf@1.16.18': + resolution: {integrity: sha512-SMdhmY1P9Kveh0MRj6vqQF98bsjnDa0lBBvkd/lcpxmjj5QHJOQAU0N3g6h7GPWpBMs3h2g3lHu/QTDn+uMsXg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + '@swc/core-linux-arm64-gnu@1.15.40': resolution: {integrity: sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + '@swc/core-linux-arm64-gnu@1.16.18': + resolution: {integrity: sha512-ep/mQyhblQYZ6tkfq0IJspcVSFl5jdywks3kejanrbB31uuujRD/QWOoGNp2L3lLRdAdQvgl/M3pA7g3hSuQug==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + '@swc/core-linux-arm64-musl@1.15.40': resolution: {integrity: sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + '@swc/core-linux-arm64-musl@1.16.18': + resolution: {integrity: sha512-pkzGVxrAQu5+febW1a/Aiph3YJk0UiJKXoWbxnDVEIOZWkk5aETHTuyVsOQ3kOgn2dzNMvJVeqcoEQdH+ntqWQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + '@swc/core-linux-ppc64-gnu@1.15.40': resolution: {integrity: sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==} engines: {node: '>=10'} cpu: [ppc64] os: [linux] + '@swc/core-linux-ppc64-gnu@1.16.18': + resolution: {integrity: sha512-v9kiu4as1pJyQ8ESFTtMNydZ99m86QXLfAtm+XiKPKS93pXL80BCaXokAs9ebioGkIXgmwfbzUCdUC62H3JWkw==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + '@swc/core-linux-s390x-gnu@1.15.40': resolution: {integrity: sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==} engines: {node: '>=10'} cpu: [s390x] os: [linux] + '@swc/core-linux-s390x-gnu@1.16.18': + resolution: {integrity: sha512-vU7TKRJ26i86YQr4QY/5bdcH72KzkBbSSUfPlErW6d6KOR47BcVGlYZFTcMMYlmd3CVSXFHW18tBSila66YfoA==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + '@swc/core-linux-x64-gnu@1.15.40': resolution: {integrity: sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==} engines: {node: '>=10'} cpu: [x64] os: [linux] + '@swc/core-linux-x64-gnu@1.16.18': + resolution: {integrity: sha512-Ml6kbe0BoVJkSgmYPAEUudGcu84jaQeqDN8xeMumbwNpT0vlmm7241Ddp9XKCjQABvjomnXF0OiNOAfjBhNDNw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + '@swc/core-linux-x64-musl@1.15.40': resolution: {integrity: sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==} engines: {node: '>=10'} cpu: [x64] os: [linux] + '@swc/core-linux-x64-musl@1.16.18': + resolution: {integrity: sha512-TGU24+QmmS8y/hg0BLKgOU3W5TCcQC9bgGl8AB0R61gTNQdWdcmCehRLAJwlZfxtAml66xMJ6PWrX8NKrABOsA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + '@swc/core-win32-arm64-msvc@1.15.40': resolution: {integrity: sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==} engines: {node: '>=10'} cpu: [arm64] os: [win32] + '@swc/core-win32-arm64-msvc@1.16.18': + resolution: {integrity: sha512-S53hm7Amc3YV8t2BUT6R7J+zSnwiGarG+GnxMca4W0uMkxBZ3Gl6jBwEgb6YqrYjf2DzOuRfauztqptCO76a/w==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + '@swc/core-win32-ia32-msvc@1.15.40': resolution: {integrity: sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] + '@swc/core-win32-ia32-msvc@1.16.18': + resolution: {integrity: sha512-+6/10+OJmU12zWkrPVG0QeQEr8VsWeF8gyXrOfHZMIfMOfz8/ULXVZPhV9GhGMKSWake1xEJj2IYCiFRS/xMpQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + '@swc/core-win32-x64-msvc@1.15.40': resolution: {integrity: sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==} engines: {node: '>=10'} cpu: [x64] os: [win32] + '@swc/core-win32-x64-msvc@1.16.18': + resolution: {integrity: sha512-7LsVrIICAZWSajXT6DwO7PYSTnW9maamOCyQylaOh13Viptrgi/7hl9fP75SiHe+kscaJahGXcorgt4MOOh/1w==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + '@swc/core@1.15.40': resolution: {integrity: sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==} engines: {node: '>=10'} @@ -1801,9 +2273,21 @@ packages: '@swc/helpers': optional: true + '@swc/core@1.16.18': + resolution: {integrity: sha512-0cYQLkpcwUngg55TDwpwrrVOU+B6zh1NFbp+oS7dVFhmYkzaARelwugo/bzPJh1FrcnppmF+KTqioCGwyqbcEw==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + '@swc/jest@0.2.39': resolution: {integrity: sha512-eyokjOwYd0Q8RnMHri+8/FS1HIrIUKK/sRrFp8c1dThUOfNeCWbLmBP1P5VsKdvmkd25JaH+OKYwEYiAYg9YAA==} engines: {npm: '>= 7.0.0'} @@ -1813,6 +2297,9 @@ packages: '@swc/types@0.1.26': resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} + '@swc/types@0.1.28': + resolution: {integrity: sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==} + '@tailwindcss/node@4.2.1': resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} @@ -2026,6 +2513,10 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + '@typespec/ts-http-runtime@0.3.9': + resolution: {integrity: sha512-edSdeAqkdxBVzA1yL1LrLCml1YjyCVvPMtMqJpbF+6K609tHe8V6sQUzFQSGcYNhcuhOceZtjvN32+mpIth30A==} + engines: {node: '>=22.0.0'} + '@unlayer/exporters@1.497.0': resolution: {integrity: sha512-erm4UsZcZkBTtbSkBehZ/pmhHKHDKT4kDISZ3JzzSi25ehZQpBJzEyiIQzEp135/7NDnAchbPgMask9qBo5X8g==} engines: {node: '>=14.0.0'} @@ -2205,6 +2696,9 @@ packages: axios@1.16.1: resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} + axios@1.20.0: + resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==} + babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2237,6 +2731,9 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + baseline-browser-mapping@2.11.28: resolution: {integrity: sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==} engines: {node: '>=6.0.0'} @@ -2391,6 +2888,9 @@ packages: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} @@ -2469,6 +2969,10 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} + engines: {node: '>=18'} + convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -2575,6 +3079,10 @@ packages: resolution: {integrity: sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==} engines: {node: '>=0.8'} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -2844,6 +3352,10 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2868,6 +3380,9 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -2886,6 +3401,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2946,6 +3465,14 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + fromentries@1.3.2: resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==} @@ -2972,6 +3499,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gaxios@7.3.1: + resolution: {integrity: sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==} + engines: {node: '>=18'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3062,6 +3593,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -3085,6 +3620,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-parse-stringify@4.0.1: + resolution: {integrity: sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==} + html-to-text@9.0.5: resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} engines: {node: '>=14'} @@ -3159,10 +3697,21 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + i18next@26.4.4: + resolution: {integrity: sha512-O+rqmQUi54OwAgnZ4SAwO6aRrbXZt36VZXWt6zvq0FpC3vJbH46oQRs6u4Uwm1vf8ZI7UqOXQiJhQVTor7vRiQ==} + peerDependencies: + typescript: ^5 || ^6 || ^7 + peerDependenciesMeta: + typescript: + optional: true + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + icu-minify@4.14.9: + resolution: {integrity: sha512-PmO+fNnC3K3mfPKTnm3RrPERrhQZnjm1AlpyZQ0ShbXi8Y/kYZ3YOde8TEjCLz9k3P2DmzkBn8lykGrCAi1Ajw==} + image-size@0.5.5: resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} engines: {node: '>=0.10.0'} @@ -3184,6 +3733,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inboundemail@0.20.0: + resolution: {integrity: sha512-hXZcd9WdJDJq9dUmSWwY4Dn7lJUKoHD4hp5UnqJg/g6ER+IUOxyqGWSCJ+RTiqyEwMQDABVJmf3OlXY/ZgVcug==} + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -3202,6 +3754,12 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + intl-messageformat@11.2.15: + resolution: {integrity: sha512-W4VNytsXyWgW5lvVchmsb9xgA5rSKqoflu3/IQ9UCtB0z4R+Bx9MoEkwFT/+30RCl/KzUoH0QGGe9lppF5SuvQ==} + + intl-messageformat@12.1.3: + resolution: {integrity: sha512-JXi5wk8R/oSECKyxSIZ00VS8KG6MZkHM0qU4NS7RrvIWWXkiv3wVqGlCvEvyXiDvKlt2yqwZMiZGIv+I8rrnZQ==} + is-arguments@1.2.0: resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} engines: {node: '>= 0.4'} @@ -3261,6 +3819,9 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -3584,6 +4145,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -3597,6 +4161,10 @@ packages: engines: {node: '>=18.17'} hasBin: true + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -3609,6 +4177,10 @@ packages: engines: {node: '>=14'} hasBin: true + lettermint@3.1.0: + resolution: {integrity: sha512-41JZf6rr3/2ZkOBktJ28dY64tWGasMONAAvV0oHc3qAc8eHk4RzpGnn2/HDZNo759toXSqmhSxeMXk2MkPiljg==} + engines: {node: '>=20'} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -3751,6 +4323,13 @@ packages: magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + mailersend@3.4.1: + resolution: {integrity: sha512-sOJ7XiCTko+JSlQys6yemACtaSJHiqb1p+0i3KieN/TH69+rnSi7GLtT5T0GDxGAj+7FcNORbxcwvvts4SCXyg==} + + mailgun.js@14.1.0: + resolution: {integrity: sha512-sVrc6RbyYYZt6EccHIBBWxsnywIfSMlywmeu6lZ5hBabJqXr8/csifibt/ygOEQ7SncULWoKU9lTEC2v/sOpUA==} + engines: {node: '>=18.0.0'} + make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} @@ -3971,9 +4550,60 @@ packages: engines: {node: '>= 4.4.x'} hasBin: true + negotiator@1.1.0: + resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} + engines: {node: '>=18'} + + next-intl-swc-plugin-extractor@4.14.9: + resolution: {integrity: sha512-8jAkmDwQJo6RDavBHm9QRl1YTn2WXf+rI4SVhMaZxZcxopnwZic6fKTXOoNIo3kzC+a6vJySkOkXmHTVFLAYtg==} + + next-intl@4.14.9: + resolution: {integrity: sha512-uauRtUC+TrYNA1OL/QcwL9irndLrI9jK3Wdaa9W0B5AeKh9cqVdNeT0IWgLnhBqBRrVwppVdI0M+C1+JdGPQ3w==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + next@16.3.8: + resolution: {integrity: sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + nock@14.0.17: + resolution: {integrity: sha512-EjRr1weMa4ALQX35AgZTEnP+weJJjlW1KGDiNM2IQC2069YDHas4f4B4UUYR+TTLyKWxJvOz2wObDKQs/LNreA==} + engines: {node: '>=18.20.0 <20 || >=20.12.1'} + node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -4055,6 +4685,9 @@ packages: resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} engines: {node: '>=0.10.0'} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -4199,6 +4832,9 @@ packages: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} + po-parser@2.2.0: + resolution: {integrity: sha512-NdTrKgh0oO7+y+RFX8KKhOB438x/j94UGXEFzl/7pHH0JjWSn42iJ9tgmPksIO6n1JKDHmNDcejPJfKspljB9g==} + portfinder@1.0.38: resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} engines: {node: '>= 10.12'} @@ -4397,6 +5033,10 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.5.30: resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==} engines: {node: ^10 || ^12 || >=14} @@ -4450,6 +5090,10 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + propagate@2.0.1: + resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} + engines: {node: '>= 8'} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -4496,6 +5140,28 @@ packages: peerDependencies: react: ^19.1.0 + react-i18next@17.0.16: + resolution: {integrity: sha512-7uImVD9hG5yUOaP5KHVied1x0A004Di/KU031ULR35Ma+9e9H5GdnNZHgQEIByyAz+IAXBkRDTedNVzjPUH8ZA==} + peerDependencies: + i18next: '>= 26.2.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 || ^6 || ^7 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + + react-intl@12.1.4: + resolution: {integrity: sha512-huK6iXamrEJ4D011VCmO+/CyzFtSKeF7eqcHW1dVe3t2wpgB/Iscl6CoMso/YLItX3Qh/gcVQMXNbbhpGbCrUw==} + peerDependencies: + '@types/react': '>=18.0.0' + react: '>=18.0.0' + react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -4671,6 +5337,15 @@ packages: resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} engines: {node: '>= 0.4'} + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -4782,6 +5457,9 @@ packages: prettier: optional: true + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -4836,6 +5514,19 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + stylehacks@7.0.11: resolution: {integrity: sha512-iODNfhXVLqc5LADs+Y6Oh5wJuK5ZcHbVng8aiK3y9pjMQdc5hLrBW0eFU6FtnpNrE6PoEg/MmFTU4waotj5WNg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} @@ -5059,6 +5750,20 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-intl@4.14.9: + resolution: {integrity: sha512-37ktvdCCz+6JY2VFPl/GV69Ds4ahiUT2c2xEZZRZYxdgGLxFsUbdxzjQVRm3YMIOUU0VPs6zvlifb4Yjk4IxIg==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.7.0: + resolution: {integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5214,6 +5919,10 @@ packages: resolution: {integrity: sha512-Ezr98sqXW/+OCGoUEXuOKVR+oVFlSdn1tIySEEJdiSAw4IjrW8hQkwARSSBJTSB5Us5dnytDgL0ZDliAYBhaNA==} engines: {node: '>=10.0.0'} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -5531,6 +6240,110 @@ snapshots: '@aws/lambda-invoke-store@0.3.0': {} + '@azure-rest/core-client@2.9.0': + dependencies: + '@azure/abort-controller': 2.2.0 + '@azure/core-auth': 1.11.0 + '@azure/core-rest-pipeline': 1.25.0 + '@azure/core-tracing': 1.4.0 + '@typespec/ts-http-runtime': 0.3.9 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/abort-controller@2.2.0': + dependencies: + tslib: 2.8.1 + + '@azure/communication-common@2.5.0': + dependencies: + '@azure-rest/core-client': 2.9.0 + '@azure/abort-controller': 2.2.0 + '@azure/core-auth': 1.11.0 + '@azure/core-rest-pipeline': 1.25.0 + '@azure/core-tracing': 1.4.0 + '@azure/core-util': 1.14.0 + '@azure/logger': 1.4.0 + events: 3.3.0 + jwt-decode: 4.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/communication-email@1.1.0': + dependencies: + '@azure/abort-controller': 2.2.0 + '@azure/communication-common': 2.5.0 + '@azure/core-auth': 1.11.0 + '@azure/core-client': 1.11.1 + '@azure/core-lro': 2.7.2 + '@azure/core-rest-pipeline': 1.25.0 + '@azure/core-util': 1.14.0 + '@azure/logger': 1.4.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-auth@1.11.0': + dependencies: + '@azure/abort-controller': 2.2.0 + '@azure/core-util': 1.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-client@1.11.1': + dependencies: + '@azure/abort-controller': 2.2.0 + '@azure/core-auth': 1.11.0 + '@azure/core-rest-pipeline': 1.25.0 + '@azure/core-tracing': 1.4.0 + '@azure/core-util': 1.14.0 + '@azure/logger': 1.4.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-lro@2.7.2': + dependencies: + '@azure/abort-controller': 2.2.0 + '@azure/core-util': 1.14.0 + '@azure/logger': 1.4.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-rest-pipeline@1.25.0': + dependencies: + '@azure/abort-controller': 2.2.0 + '@azure/core-auth': 1.11.0 + '@azure/core-tracing': 1.4.0 + '@azure/core-util': 1.14.0 + '@azure/logger': 1.4.0 + '@typespec/ts-http-runtime': 0.3.9 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-tracing@1.4.0': + dependencies: + tslib: 2.8.1 + + '@azure/core-util@1.14.0': + dependencies: + '@azure/abort-controller': 2.2.0 + '@typespec/ts-http-runtime': 0.3.9 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/logger@1.4.0': + dependencies: + '@typespec/ts-http-runtime': 0.3.9 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -5717,6 +6530,8 @@ snapshots: '@babel/runtime@7.28.6': {} + '@babel/runtime@7.29.10': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.29.0 @@ -5766,6 +6581,22 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@eloqnt/config@0.1.1': {} + + '@eloqnt/format-json@0.1.0': + dependencies: + '@eloqnt/config': 0.1.1 + + '@eloqnt/format-po@0.1.1': + dependencies: + '@eloqnt/config': 0.1.1 + po-parser: 2.2.0 + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.25.11': optional: true @@ -5919,30 +6750,168 @@ snapshots: '@esbuild/win32-x64@0.25.11': optional: true - '@esbuild/win32-x64@0.27.3': + '@esbuild/win32-x64@0.27.3': + optional: true + + '@eusend_dev/sdk@0.19.0(@react-email/render@2.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + optionalDependencies: + '@react-email/render': 2.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + + '@formatjs/fast-memoize@3.1.7': {} + + '@formatjs/icu-messageformat-parser@3.5.18': + dependencies: + '@formatjs/icu-skeleton-parser': 2.1.11 + + '@formatjs/icu-messageformat-parser@3.5.21': + dependencies: + '@formatjs/icu-skeleton-parser': 2.1.12 + + '@formatjs/icu-skeleton-parser@2.1.11': {} + + '@formatjs/icu-skeleton-parser@2.1.12': {} + + '@formatjs/intl-localematcher@0.8.14': + dependencies: + '@formatjs/fast-memoize': 3.1.7 + + '@formatjs/intl@6.1.3': + dependencies: + '@formatjs/fast-memoize': 3.1.7 + '@formatjs/icu-messageformat-parser': 3.5.21 + intl-messageformat: 12.1.3 + + '@getbrevo/brevo@6.1.0': {} + + '@hapi/address@5.1.1': + dependencies: + '@hapi/hoek': 11.0.7 + + '@hapi/formula@3.0.2': {} + + '@hapi/hoek@11.0.7': {} + + '@hapi/hoek@9.3.0': {} + + '@hapi/pinpoint@2.0.1': {} + + '@hapi/tlds@1.1.6': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + + '@hapi/topo@6.0.2': + dependencies: + '@hapi/hoek': 11.0.7 + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.4 + optional: true + + '@img/sharp-darwin-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.4 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + optional: true + + '@img/sharp-linux-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.4 + optional: true + + '@img/sharp-linux-arm@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.4 + optional: true + + '@img/sharp-linux-ppc64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.4 + optional: true + + '@img/sharp-linux-riscv64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.4 + optional: true + + '@img/sharp-linux-s390x@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.4 + optional: true + + '@img/sharp-linux-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 optional: true - '@hapi/address@5.1.1': + '@img/sharp-wasm32@0.35.5': dependencies: - '@hapi/hoek': 11.0.7 - - '@hapi/formula@3.0.2': {} - - '@hapi/hoek@11.0.7': {} - - '@hapi/hoek@9.3.0': {} + '@emnapi/runtime': 1.11.3 + optional: true - '@hapi/pinpoint@2.0.1': {} + '@img/sharp-webcontainers-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true - '@hapi/tlds@1.1.6': {} + '@img/sharp-win32-arm64@0.35.5': + optional: true - '@hapi/topo@5.1.0': - dependencies: - '@hapi/hoek': 9.3.0 + '@img/sharp-win32-ia32@0.35.5': + optional: true - '@hapi/topo@6.0.2': - dependencies: - '@hapi/hoek': 11.0.7 + '@img/sharp-win32-x64@0.35.5': + optional: true '@isaacs/cliui@8.0.2': dependencies: @@ -6251,8 +7220,52 @@ snapshots: '@types/react': 18.3.26 react: 18.3.1 + '@mswjs/interceptors@0.41.9': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@next/env@16.3.8': {} + + '@next/swc-darwin-arm64@16.3.8': + optional: true + + '@next/swc-darwin-x64@16.3.8': + optional: true + + '@next/swc-linux-arm64-gnu@16.3.8': + optional: true + + '@next/swc-linux-arm64-musl@16.3.8': + optional: true + + '@next/swc-linux-x64-gnu@16.3.8': + optional: true + + '@next/swc-linux-x64-musl@16.3.8': + optional: true + + '@next/swc-win32-arm64-msvc@16.3.8': + optional: true + + '@next/swc-win32-x64-msvc@16.3.8': + optional: true + '@one-ini/wasm@0.2.1': {} + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -6312,7 +7325,6 @@ snapshots: '@parcel/watcher-win32-arm64': 2.5.6 '@parcel/watcher-win32-ia32': 2.5.6 '@parcel/watcher-win32-x64': 2.5.6 - optional: true '@pkgjs/parseargs@0.11.0': optional: true @@ -6519,6 +7531,22 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.52.5': optional: true + '@scaleway/random-name@6.0.2': {} + + '@scaleway/sdk-client@2.14.0': {} + + '@scaleway/sdk-std@2.9.3(@scaleway/sdk-client@2.14.0)': + dependencies: + '@scaleway/sdk-client': 2.14.0 + + '@scaleway/sdk-tem@2.16.1(@scaleway/sdk-client@2.14.0)': + dependencies: + '@scaleway/random-name': 6.0.2 + '@scaleway/sdk-client': 2.14.0 + '@scaleway/sdk-std': 2.9.3(@scaleway/sdk-client@2.14.0) + + '@schummar/icu-type-parser@1.21.5': {} + '@selderee/plugin-htmlparser2@0.11.0': dependencies: domhandler: 5.0.3 @@ -6673,15 +7701,15 @@ snapshots: optionalDependencies: typescript: 5.9.3 - '@storybook/test-runner@0.23.0(@types/node@24.9.1)(storybook@9.1.16(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.31.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.21.0)))': + '@storybook/test-runner@0.23.0(@swc/helpers@0.5.23)(@types/node@24.9.1)(storybook@9.1.16(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.31.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.21.0)))': dependencies: '@babel/core': 7.28.5 '@babel/generator': 7.28.5 '@babel/template': 7.27.2 '@babel/types': 7.28.5 '@jest/types': 29.6.3 - '@swc/core': 1.15.40 - '@swc/jest': 0.2.39(@swc/core@1.15.40) + '@swc/core': 1.15.40(@swc/helpers@0.5.23) + '@swc/jest': 0.2.39(@swc/core@1.15.40(@swc/helpers@0.5.23)) expect-playwright: 0.8.0 jest: 29.7.0(@types/node@24.9.1) jest-circus: 29.7.0 @@ -6706,40 +7734,76 @@ snapshots: '@swc/core-darwin-arm64@1.15.40': optional: true + '@swc/core-darwin-arm64@1.16.18': + optional: true + '@swc/core-darwin-x64@1.15.40': optional: true + '@swc/core-darwin-x64@1.16.18': + optional: true + '@swc/core-linux-arm-gnueabihf@1.15.40': optional: true + '@swc/core-linux-arm-gnueabihf@1.16.18': + optional: true + '@swc/core-linux-arm64-gnu@1.15.40': optional: true + '@swc/core-linux-arm64-gnu@1.16.18': + optional: true + '@swc/core-linux-arm64-musl@1.15.40': optional: true + '@swc/core-linux-arm64-musl@1.16.18': + optional: true + '@swc/core-linux-ppc64-gnu@1.15.40': optional: true + '@swc/core-linux-ppc64-gnu@1.16.18': + optional: true + '@swc/core-linux-s390x-gnu@1.15.40': optional: true + '@swc/core-linux-s390x-gnu@1.16.18': + optional: true + '@swc/core-linux-x64-gnu@1.15.40': optional: true + '@swc/core-linux-x64-gnu@1.16.18': + optional: true + '@swc/core-linux-x64-musl@1.15.40': optional: true + '@swc/core-linux-x64-musl@1.16.18': + optional: true + '@swc/core-win32-arm64-msvc@1.15.40': optional: true + '@swc/core-win32-arm64-msvc@1.16.18': + optional: true + '@swc/core-win32-ia32-msvc@1.15.40': optional: true + '@swc/core-win32-ia32-msvc@1.16.18': + optional: true + '@swc/core-win32-x64-msvc@1.15.40': optional: true - '@swc/core@1.15.40': + '@swc/core-win32-x64-msvc@1.16.18': + optional: true + + '@swc/core@1.15.40(@swc/helpers@0.5.23)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.26 @@ -6756,13 +7820,37 @@ snapshots: '@swc/core-win32-arm64-msvc': 1.15.40 '@swc/core-win32-ia32-msvc': 1.15.40 '@swc/core-win32-x64-msvc': 1.15.40 + '@swc/helpers': 0.5.23 + + '@swc/core@1.16.18(@swc/helpers@0.5.23)': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.28 + optionalDependencies: + '@swc/core-darwin-arm64': 1.16.18 + '@swc/core-darwin-x64': 1.16.18 + '@swc/core-linux-arm-gnueabihf': 1.16.18 + '@swc/core-linux-arm64-gnu': 1.16.18 + '@swc/core-linux-arm64-musl': 1.16.18 + '@swc/core-linux-ppc64-gnu': 1.16.18 + '@swc/core-linux-s390x-gnu': 1.16.18 + '@swc/core-linux-x64-gnu': 1.16.18 + '@swc/core-linux-x64-musl': 1.16.18 + '@swc/core-win32-arm64-msvc': 1.16.18 + '@swc/core-win32-ia32-msvc': 1.16.18 + '@swc/core-win32-x64-msvc': 1.16.18 + '@swc/helpers': 0.5.23 '@swc/counter@0.1.3': {} - '@swc/jest@0.2.39(@swc/core@1.15.40)': + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@swc/jest@0.2.39(@swc/core@1.15.40(@swc/helpers@0.5.23))': dependencies: '@jest/create-cache-key-function': 30.4.1 - '@swc/core': 1.15.40 + '@swc/core': 1.15.40(@swc/helpers@0.5.23) '@swc/counter': 0.1.3 jsonc-parser: 3.3.1 @@ -6770,6 +7858,10 @@ snapshots: dependencies: '@swc/counter': 0.1.3 + '@swc/types@0.1.28': + dependencies: + '@swc/counter': 0.1.3 + '@tailwindcss/node@4.2.1': dependencies: '@jridgewell/remapping': 2.3.5 @@ -6993,6 +8085,14 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 + '@typespec/ts-http-runtime@0.3.9': + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@unlayer/exporters@1.497.0(@unlayer/types@1.497.0)': dependencies: '@lexical/headless': 0.41.0 @@ -7228,6 +8328,16 @@ snapshots: - debug - supports-color + axios@1.20.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.6 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + babel-jest@29.7.0(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 @@ -7287,6 +8397,8 @@ snapshots: balanced-match@4.0.4: {} + base-64@1.0.0: {} + baseline-browser-mapping@2.11.28: {} baseline-browser-mapping@2.8.20: {} @@ -7464,6 +8576,8 @@ snapshots: clean-stack@2.2.0: {} + client-only@0.0.1: {} + cliui@6.0.0: dependencies: string-width: 4.2.3 @@ -7533,6 +8647,8 @@ snapshots: consola@3.4.2: {} + content-type@2.1.0: {} + convert-source-map@1.9.0: {} convert-source-map@2.0.0: {} @@ -7680,6 +8796,8 @@ snapshots: find-pkg: 0.1.2 fs-exists-sync: 0.1.0 + data-uri-to-buffer@4.0.1: {} + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -7986,6 +9104,8 @@ snapshots: eventemitter3@4.0.7: {} + events@3.3.0: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -8016,6 +9136,8 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 + extend@3.0.2: {} + fast-json-stable-stringify@2.1.0: {} fast-sha256@1.3.0: {} @@ -8028,6 +9150,11 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -8102,6 +9229,18 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + fromentries@1.3.2: {} fs-exists-sync@0.1.0: {} @@ -8118,6 +9257,14 @@ snapshots: functions-have-names@1.2.3: {} + gaxios@7.3.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -8225,6 +9372,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + he@1.2.0: {} highlight.js@11.12.0: {} @@ -8243,6 +9394,8 @@ snapshots: html-escaper@2.0.2: {} + html-parse-stringify@4.0.1: {} + html-to-text@9.0.5: dependencies: '@selderee/plugin-htmlparser2': 0.11.0 @@ -8352,10 +9505,18 @@ snapshots: human-signals@2.1.0: {} + i18next@26.4.4(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 + icu-minify@4.14.9: + dependencies: + '@formatjs/icu-messageformat-parser': 3.5.21 + image-size@0.5.5: optional: true @@ -8374,6 +9535,13 @@ snapshots: imurmurhash@0.1.4: {} + inboundemail@0.20.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@react-email/render': 2.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - react + - react-dom + indent-string@4.0.0: {} inflight@1.0.6: @@ -8391,6 +9559,16 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + intl-messageformat@11.2.15: + dependencies: + '@formatjs/fast-memoize': 3.1.7 + '@formatjs/icu-messageformat-parser': 3.5.18 + + intl-messageformat@12.1.3: + dependencies: + '@formatjs/fast-memoize': 3.1.7 + '@formatjs/icu-messageformat-parser': 3.5.21 + is-arguments@1.2.0: dependencies: call-bound: 1.0.4 @@ -8426,8 +9604,7 @@ snapshots: is-docker@2.2.1: {} - is-extglob@2.1.1: - optional: true + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -8436,12 +9613,13 @@ snapshots: is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - optional: true is-json@2.0.1: {} is-map@2.0.3: {} + is-node-process@1.2.0: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -9019,6 +10197,8 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-stringify-safe@5.0.1: {} + json5@2.2.3: {} jsonc-parser@3.3.1: {} @@ -9032,6 +10212,8 @@ snapshots: slick: 1.12.2 web-resource-inliner: 8.0.0 + jwt-decode@4.0.0: {} + kleur@3.0.3: {} leac@0.6.0: {} @@ -9051,6 +10233,8 @@ snapshots: source-map: 0.6.1 optional: true + lettermint@3.1.0: {} + leven@3.1.0: {} lexical@0.41.0: {} @@ -9156,6 +10340,22 @@ snapshots: '@babel/types': 7.28.5 source-map-js: 1.2.1 + mailersend@3.4.1: + dependencies: + gaxios: 7.3.1 + qs: 6.15.2 + transitivePeerDependencies: + - supports-color + + mailgun.js@14.1.0: + dependencies: + axios: 1.20.0 + base-64: 1.0.0 + url-join: 4.0.1 + transitivePeerDependencies: + - debug + - supports-color + make-dir@2.1.0: dependencies: pify: 4.0.1 @@ -9713,8 +10913,72 @@ snapshots: sax: 1.6.1 optional: true - node-addon-api@7.1.1: - optional: true + negotiator@1.1.0: + dependencies: + content-type: 2.1.0 + + next-intl-swc-plugin-extractor@4.14.9: {} + + next-intl@4.14.9(@swc/helpers@0.5.23)(@types/react@18.3.26)(next@16.3.8(@types/node@24.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.93.2))(react@18.3.1): + dependencies: + '@eloqnt/config': 0.1.1 + '@eloqnt/format-json': 0.1.0 + '@eloqnt/format-po': 0.1.1 + '@formatjs/intl-localematcher': 0.8.14 + '@parcel/watcher': 2.5.6 + '@swc/core': 1.16.18(@swc/helpers@0.5.23) + icu-minify: 4.14.9 + negotiator: 1.1.0 + next: 16.3.8(@types/node@24.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.93.2) + next-intl-swc-plugin-extractor: 4.14.9 + react: 18.3.1 + use-intl: 4.14.9(@types/react@18.3.26)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.26 + transitivePeerDependencies: + - '@swc/helpers' + + next@16.3.8(@types/node@24.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.93.2): + dependencies: + '@next/env': 16.3.8 + '@swc/helpers': 0.5.23 + baseline-browser-mapping: 2.11.28 + caniuse-lite: 1.0.30001816 + postcss: 8.5.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.6(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 16.3.8 + '@next/swc-darwin-x64': 16.3.8 + '@next/swc-linux-arm64-gnu': 16.3.8 + '@next/swc-linux-arm64-musl': 16.3.8 + '@next/swc-linux-x64-gnu': 16.3.8 + '@next/swc-linux-x64-musl': 16.3.8 + '@next/swc-win32-arm64-msvc': 16.3.8 + '@next/swc-win32-x64-msvc': 16.3.8 + sass: 1.93.2 + sharp: 0.35.5(@types/node@24.9.1) + transitivePeerDependencies: + - '@babel/core' + - '@types/node' + - babel-plugin-macros + + nock@14.0.17: + dependencies: + '@mswjs/interceptors': 0.41.9 + json-stringify-safe: 5.0.1 + propagate: 2.0.1 + + node-addon-api@7.1.1: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 node-int64@0.4.0: {} @@ -9814,6 +11078,8 @@ snapshots: os-homedir@1.0.2: {} + outvariant@1.4.3: {} + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -9944,6 +11210,8 @@ snapshots: pngjs@7.0.0: {} + po-parser@2.2.0: {} + portfinder@1.0.38: dependencies: async: 3.2.6 @@ -10121,6 +11389,12 @@ snapshots: postcss-value-parser@4.2.0: {} + postcss@8.5.23: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + postcss@8.5.30: dependencies: nanoid: 3.3.20 @@ -10184,6 +11458,8 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + propagate@2.0.1: {} + proto-list@1.2.4: {} proxy-from-env@2.1.0: {} @@ -10235,6 +11511,25 @@ snapshots: react: 18.3.1 scheduler: 0.26.0 + react-i18next@17.0.16(i18next@26.4.4(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.29.10 + html-parse-stringify: 4.0.1 + i18next: 26.4.4(typescript@5.9.3) + react: 18.3.1 + use-sync-external-store: 1.7.0(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + typescript: 5.9.3 + + react-intl@12.1.4(@types/react@18.3.26)(react@18.3.1): + dependencies: + '@formatjs/icu-messageformat-parser': 3.5.21 + '@formatjs/intl': 6.1.3 + '@types/react': 18.3.26 + intl-messageformat: 12.1.3 + react: 18.3.1 + react-is@17.0.2: {} react-is@18.3.1: {} @@ -10423,6 +11718,40 @@ snapshots: functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 + sharp@0.35.5(@types/node@24.9.1): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 + '@types/node': 24.9.1 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -10556,6 +11885,8 @@ snapshots: - utf-8-validate - vite + strict-event-emitter@0.5.1: {} + string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -10608,6 +11939,11 @@ snapshots: dependencies: js-tokens: 9.0.1 + styled-jsx@5.1.6(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + stylehacks@7.0.11(postcss@8.5.30): dependencies: browserslist: 4.29.3 @@ -10734,7 +12070,36 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.0(@swc/core@1.15.40)(jiti@2.6.1)(postcss@8.5.30)(tsx@4.21.0)(typescript@5.9.3): + tsup@8.5.0(@swc/core@1.15.40(@swc/helpers@0.5.23))(jiti@2.6.1)(postcss@8.5.30)(tsx@4.21.0)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.25.11) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.25.11 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.30)(tsx@4.21.0) + resolve-from: 5.0.0 + rollup: 4.52.5 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.15.40(@swc/helpers@0.5.23) + postcss: 8.5.30 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsup@8.5.0(@swc/core@1.16.18(@swc/helpers@0.5.23))(jiti@2.6.1)(postcss@8.5.30)(tsx@4.21.0)(typescript@5.9.3): dependencies: bundle-require: 5.1.0(esbuild@0.25.11) cac: 6.7.14 @@ -10754,7 +12119,7 @@ snapshots: tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: - '@swc/core': 1.15.40 + '@swc/core': 1.16.18(@swc/helpers@0.5.23) postcss: 8.5.30 typescript: 5.9.3 transitivePeerDependencies: @@ -10827,6 +12192,20 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 + use-intl@4.14.9(@types/react@18.3.26)(react@18.3.1): + dependencies: + '@formatjs/fast-memoize': 3.1.7 + '@schummar/icu-type-parser': 1.21.5 + icu-minify: 4.14.9 + intl-messageformat: 11.2.15 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.26 + + use-sync-external-store@1.7.0(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} uuid@8.3.2: {} @@ -11067,6 +12446,8 @@ snapshots: mime: 2.6.0 valid-data-url: 3.0.1 + web-streams-polyfill@3.3.3: {} + webidl-conversions@4.0.2: {} webidl-conversions@7.0.0: {} From 6e2d6b2a05f9d992e283be4256c08b0be5c31183 Mon Sep 17 00:00:00 2001 From: Ivo Date: Fri, 16 Oct 2026 08:02:54 +0200 Subject: [PATCH 2/8] docs(react): add guides for email providers, i18n and monorepos The monorepo guide previews templates with a small render script, so it needs nothing that isn't published. The READMEs point to the Elements skill in unlayer/unlayer-skills, and list @unlayer/migrate as published, with its MJML and Beefree sources. Co-Authored-By: Claude Code (Claude Opus 5.5 xhigh) --- README.md | 11 +- packages/react/README.md | 4 +- packages/react/docs/README.md | 9 +- packages/react/docs/email-providers.md | 293 ++++++++++++++++++++- packages/react/docs/i18n.md | 239 +++++++++++++++++ packages/react/docs/monorepo.md | 260 ++++++++++++++++++ packages/react/docs/transactional-email.md | 2 +- 7 files changed, 807 insertions(+), 11 deletions(-) create mode 100644 packages/react/docs/i18n.md create mode 100644 packages/react/docs/monorepo.md diff --git a/README.md b/README.md index 03adc8df..c8a6ea7a 100644 --- a/README.md +++ b/README.md @@ -261,12 +261,14 @@ import { Row, Column, ColumnLayouts } from '@unlayer/react-elements'; For the full API reference, component props, design patterns, and common mistakes, see the **[React package documentation](./packages/react/README.md)**. +Using a coding agent? Install the official **[`unlayer-elements` skill](https://github.com/unlayer/unlayer-skills)** (`npx skills add unlayer/unlayer-skills`): the structure, layouts, renderers, preview commands, and common mistakes. + ## Packages | Package | Description | Published | |---------|-------------|-----------| | [`@unlayer/react-elements`](./packages/react) | React components and renderers | [![npm](https://img.shields.io/npm/v/@unlayer/react-elements.svg)](https://www.npmjs.com/package/@unlayer/react-elements) | -| [`@unlayer/migrate`](./packages/migrate) | Migrate React Email templates to Elements, checked against the originals | Not yet | +| [`@unlayer/migrate`](./packages/migrate) | Migrate React Email, MJML and Beefree templates to Elements, checked against the originals | [![npm](https://img.shields.io/npm/v/@unlayer/migrate.svg)](https://www.npmjs.com/package/@unlayer/migrate) | | [`@unlayer/from-react-email`](./packages/from-react-email) | The React Email → Elements converter, as a library | Private (bundled into `@unlayer/migrate`, API at `@unlayer/migrate/react-email`) | | [`@unlayer/convert-core`](./packages/convert-core) | Shared model for converters (Elements tree, report, TSX printer, layout check) | Internal | | [`@unlayer-internal/shared-elements`](./packages/shared) | Framework-agnostic shared logic | Internal | @@ -278,7 +280,8 @@ Runnable projects you can clone and experiment with: | Example | Description | |---------|-------------| -| [`examples/content-workflows`](./examples/content-workflows) | One React invoice → email + web + real PDF, five email providers, and editable design JSON | +| [`examples/content-workflows`](./examples/content-workflows) | One React invoice → email + web + real PDF, fourteen email providers, and editable design JSON | +| [`examples/i18n`](./examples/i18n) | One email in English and Arabic with react-intl, react-i18next, and next-intl, tested for translated text, `lang`, and `dir` | | [`examples/nextjs-app-router`](./examples/nextjs-app-router) | Next.js 15 App Router — renders an email with `renderToHtml()` in a Server Component, previews it at `/email-preview`, and serves the raw HTML from a Route Handler | ## Development @@ -342,8 +345,10 @@ Yes. `registerElementsTool()` turns a custom tool definition into a first-class Start with [when to use Elements](./packages/react/docs/README.md), or go directly to a runnable workflow: - [Build transactional emails in React](./packages/react/docs/transactional-email.md) — HTML and plain-text receipts, order confirmations, and password reset patterns. -- [Send with Resend, AWS SES, Postmark, SendGrid, or Nodemailer](./packages/react/docs/email-providers.md) — provider setup and complete server-side integrations. +- [Send with Resend, AWS SES, Postmark, SendGrid, Nodemailer, Mailgun, Brevo, and more](./packages/react/docs/email-providers.md) — provider setup and complete server-side integrations for fourteen providers. - [One React invoice to email, web, and PDF](./packages/react/docs/one-template-email-web-pdf.md) — share content across all three output modes. +- [Translate emails with react-intl, react-i18next, or next-intl](./packages/react/docs/i18n.md) — load messages before rendering, and set language, direction, and alignment for right-to-left locales. +- [Use Elements in a monorepo](./packages/react/docs/monorepo.md) — a shared emails package with npm, pnpm, Yarn, or Bun, and one copy of React. - [Generate a real PDF with Playwright](./packages/react/docs/react-to-pdf.md) — print document HTML with Chromium. - [Make code and AI-generated templates visually editable](./packages/react/docs/visual-editing.md) — `renderToJson()` → Unlayer `loadDesign()` → edited JSON and HTML. diff --git a/packages/react/README.md b/packages/react/README.md index 87bc7b3f..8f2d5169 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -67,8 +67,10 @@ Agents can also preview Elements code without installing anything through the [E ## Task guides and runnable examples - [Transactional email in React](https://github.com/unlayer/elements/blob/main/packages/react/docs/transactional-email.md): render HTML and plain text, then send a receipt. -- [Resend, AWS SES, Postmark, SendGrid, and Nodemailer](https://github.com/unlayer/elements/blob/main/packages/react/docs/email-providers.md): complete provider integrations. +- [Resend, AWS SES, Postmark, SendGrid, Nodemailer, Mailgun, Brevo, and more](https://github.com/unlayer/elements/blob/main/packages/react/docs/email-providers.md): complete integrations for fourteen email providers. - [One React template to email, web, and PDF](https://github.com/unlayer/elements/blob/main/packages/react/docs/one-template-email-web-pdf.md): shared invoice content with output-specific roots. +- [Translations with react-intl, react-i18next, and next-intl](https://github.com/unlayer/elements/blob/main/packages/react/docs/i18n.md): load messages before rendering, and right-to-left emails. +- [Monorepos with npm, pnpm, Yarn, and Bun](https://github.com/unlayer/elements/blob/main/packages/react/docs/monorepo.md): a shared emails package that apps import, and one copy of React. - [React invoice PDF with Playwright](https://github.com/unlayer/elements/blob/main/packages/react/docs/react-to-pdf.md): generate actual PDF bytes from document HTML. - [React to Unlayer visual editing](https://github.com/unlayer/elements/blob/main/packages/react/docs/visual-editing.md): `renderToJson()`, editor loading, export, and supported tree constraints. - [Tailwind CSS in Elements](https://github.com/unlayer/elements/blob/main/packages/react/docs/tailwind.md): `className` with Tailwind classes on any component, as React Email's ``, kept as props in the HTML and the design. diff --git a/packages/react/docs/README.md b/packages/react/docs/README.md index 74623b33..3f8cdc69 100644 --- a/packages/react/docs/README.md +++ b/packages/react/docs/README.md @@ -11,8 +11,10 @@ npm install @unlayer/react-elements react react-dom | Problem | Guide | Result | |---------|-------|--------| | Send a React transactional email, receipt, or order confirmation | [Transactional email in React](./transactional-email.md) | Complete HTML, plain text, and provider-ready message | -| Deliver Elements HTML with an email provider | [Resend, AWS SES, Postmark, SendGrid, and Nodemailer](./email-providers.md) | Five runnable server-side integrations | +| Deliver Elements HTML with an email provider | [Resend, AWS SES, Postmark, SendGrid, Nodemailer, Mailgun, Azure, MailerSend, Scaleway, Plunk, Lettermint, Brevo, Inbound, and eusend](./email-providers.md) | Fourteen runnable server-side integrations | | Share one React template across email, web, and PDF | [One React invoice, three outputs](./one-template-email-web-pdf.md) | Email HTML, responsive page, print HTML, and real PDF | +| Translate an email and support right-to-left languages | [i18n with react-intl, react-i18next, and next-intl](./i18n.md) | English and Arabic renders with `lang`, `dir`, and alignment checked | +| Share templates across apps in a workspace | [Monorepos with npm, pnpm, Yarn, and Bun](./monorepo.md) | One emails package that apps import, and a single copy of React | | Generate an invoice PDF using Playwright | [React to PDF with Playwright](./react-to-pdf.md) | A PDF file created by headless Chromium | | Make AI-generated React content visually editable | [React to Unlayer design JSON](./visual-editing.md) | `renderToJson()` → `loadDesign()` → edited JSON and HTML | | Turn a template made in the Unlayer editor into React code | [Unlayer designs as Elements code](./codegen.md) | `designToElements(design)` → a `.tsx` component that renders back to the same design | @@ -22,6 +24,9 @@ npm install @unlayer/react-elements react react-dom | Use a web font | [Web fonts in Elements](./fonts.md) | `` in the head, the template's font, Outlook's fallback | | Render templates that load data (async components, Suspense) | [Async templates and readable HTML](./async.md) | `await renderToHtml()` from `@unlayer/react-elements/async`, with `pretty` | | Move React Email templates to Elements | [Migrate from React Email](./migrate-from-react-email.md) | `npx @unlayer/migrate`: converted templates, checked against the originals | +| Move MJML templates to Elements | [Migrate from MJML](./migrate-from-mjml.md) | `npx @unlayer/migrate --from mjml`: converted templates, checked against MJML's own HTML | +| Move Beefree templates to Elements | [Migrate from Beefree](./migrate-from-beefree.md) | `npx @unlayer/migrate --from beefree`: converted templates, checked against Beefree's HTML export | +| Upgrade from Elements 0.2.x or 0.1.x | [Upgrade Elements](./upgrading.md) | What changed in 0.3 and in 0.2, and what to check; see also the [changelog](../CHANGELOG.md) | | Open React Email templates in the Unlayer editor | [React Email in the editor](../../../examples/react-email-in-editor) | `npx @unlayer/migrate --design` → `loadDesign()` | | Try Elements or preview a snippet without installing | [Elements Playground](https://examples.unlayer.com/examples/elements-playground/) | Live email, page, or document preview, HTML and design JSON export, and the Unlayer builder | @@ -44,4 +49,4 @@ Elements supplies components and renderers. Your email provider handles delivery - Read the [visual editing constraints](./visual-editing.md#constraints) before promising React-to-editor round trips. Edited JSON doesn't change the JSX it came from: convert it to new Elements code with `designToElements()` from `@unlayer/react-elements/codegen` ([guide](./codegen.md)). - To preview code without installing, open the [Elements Playground](https://examples.unlayer.com/examples/elements-playground/) with the source in the URL hash: `#code=`, optionally with `&mode=email|page|document`. The playground takes plain JSX with no imports. In browsers with WebMCP, call its `render_elements` tool to get the HTML. -The [runnable example](../../../examples/content-workflows) backs these guides. The [React API reference](../README.md) covers component props; [Next.js App Router](../../../examples/nextjs-app-router) demonstrates framework integration. +The [runnable example](../../../examples/content-workflows) and the [i18n example](../../../examples/i18n) back these guides. Coding agents can load the [`unlayer-elements` skill](https://github.com/unlayer/unlayer-skills) (`npx skills add unlayer/unlayer-skills`), a short set of rules for writing correct templates. The [React API reference](../README.md) covers component props; [Next.js App Router](../../../examples/nextjs-app-router) demonstrates framework integration. diff --git a/packages/react/docs/email-providers.md b/packages/react/docs/email-providers.md index 85e7792e..dc3b49ec 100644 --- a/packages/react/docs/email-providers.md +++ b/packages/react/docs/email-providers.md @@ -1,6 +1,27 @@ -# Send React email with Resend, AWS SES, Postmark, SendGrid, or Nodemailer - -Use these integrations to deliver HTML and plain text rendered by `@unlayer/react-elements`. Each provider receives the same [transactional invoice message](./transactional-email.md#create-the-provider-ready-message). Pass the rendered `html` string, including its head styles, to the provider; the provider does not need to render the React tree again. +# Send React email with Resend, AWS SES, Postmark, SendGrid, Nodemailer, Mailgun, Brevo, and more + +Use these integrations to deliver HTML and plain text rendered by `@unlayer/react-elements`. Each of the fourteen providers below receives the same [transactional invoice message](./transactional-email.md#create-the-provider-ready-message). Pass the rendered `html` string, including its head styles, to the provider; the provider does not need to render the React tree again. + +## Providers + +| Provider | Install | Credentials | Command | +|----------|---------|-------------|---------| +| [Resend](#resend) | `npm install resend` | `RESEND_API_KEY` | `send:resend` | +| [AWS SES v2](#aws-ses-v2) | `npm install @aws-sdk/client-sesv2` | `AWS_REGION` and the AWS credential chain | `send:ses` | +| [Postmark](#postmark) | `npm install postmark` | `POSTMARK_SERVER_TOKEN` | `send:postmark` | +| [SendGrid](#sendgrid) | `npm install @sendgrid/mail` | `SENDGRID_API_KEY` | `send:sendgrid` | +| [Nodemailer / SMTP](#nodemailer--smtp) | `npm install nodemailer` | `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD` | `send:nodemailer` | +| [Mailgun](#mailgun) | `npm install mailgun.js` | `MAILGUN_API_KEY`, `MAILGUN_DOMAIN`, optional `MAILGUN_URL` | `send:mailgun` | +| [Azure Communication Services Email](#azure-communication-services-email) | `npm install @azure/communication-email` | `AZURE_COMMUNICATION_CONNECTION_STRING` | `send:azure` | +| [MailerSend](#mailersend) | `npm install mailersend` | `MAILERSEND_API_KEY` | `send:mailersend` | +| [Scaleway Transactional Email](#scaleway-transactional-email) | `npm install @scaleway/sdk-tem @scaleway/sdk-client` | `SCW_ACCESS_KEY`, `SCW_SECRET_KEY`, `SCW_DEFAULT_PROJECT_ID` | `send:scaleway` | +| [Plunk](#plunk) | none (REST API with `fetch`) | `PLUNK_SECRET_KEY` | `send:plunk` | +| [Lettermint](#lettermint) | `npm install lettermint` | `LETTERMINT_PROJECT_TOKEN` | `send:lettermint` | +| [Brevo](#brevo) | `npm install @getbrevo/brevo` | `BREVO_API_KEY` | `send:brevo` | +| [Inbound](#inbound) | `npm install inboundemail` | `INBOUND_API_KEY` | `send:inbound` | +| [eusend](#eusend) | `npm install @eusend_dev/sdk` | `EUSEND_API_KEY` | `send:eusend` | + +Every provider gets the HTML document and its plain-text alternative, except Plunk, whose API takes HTML only and derives the text part itself. The example pins each SDK it added to an exact version (see [package.json](../../../examples/content-workflows/package.json)); the snippets were checked against those versions' type definitions. ## Shared setup @@ -21,7 +42,13 @@ export EMAIL_TO='your-test-inbox@example.com' Replace both addresses. Set the provider-specific variables below in the process environment or your deployment's secret manager. The scripts do not load `.env` files automatically. No credentials belong in browser code. For standalone use, install `@unlayer/react-elements react react-dom`, add `tsx` to dev dependencies, set `"type": "module"` and `"jsx": "react-jsx"`, and copy the shared files plus your chosen provider file. Then use `npx tsx src/providers/.ts`. -SDK errors are allowed to fail the process; Resend's returned `error` is explicitly thrown. A successful ID/status means the provider accepted the request, not that the recipient received it. Use provider events for delivery/bounce tracking and application-level deduplication for retries. +SDK errors are allowed to fail the process. Where a provider reports a failure as a return value instead (Resend's and eusend's `error`, a failed Azure send, Plunk's error response), the script throws it. A successful ID/status means the provider accepted the request, not that the recipient received it. Use provider events for delivery/bounce tracking and application-level deduplication for retries. + +The example's tests run the Mailgun, Azure, MailerSend, Scaleway, Plunk, Lettermint, Brevo, Inbound, and eusend scripts as they are, with [nock](https://github.com/nock/nock) answering their HTTP requests: no network and no credentials. Each test checks that the request carries the credential, the sender, the recipient, the subject, the rendered HTML and the plain text (where the API takes one), and that the script prints the ID the provider returns: + +```bash +pnpm --filter @unlayer/content-workflows-example test +``` ## Resend @@ -169,4 +196,262 @@ pnpm --filter @unlayer/content-workflows-example send:nodemailer Expected: `SMTP accepted: `. +## Mailgun + +Install in your own project: `npm install mailgun.js`. The workspace example already declares this dependency. + +Set `MAILGUN_API_KEY` and `MAILGUN_DOMAIN` (your verified sending domain, for example `mg.example.com`). For a domain in Mailgun's EU region, also set `MAILGUN_URL=https://api.eu.mailgun.net`. The client uses the `FormData` built into Node.js 18 and later. [Mailgun Node.js SDK](https://documentation.mailgun.com/docs/mailgun/sdk/nodejs_sdk/). + +```ts +import Mailgun from "mailgun.js"; +import { deliveryMessage, requireEnv } from "../message"; + +// mailgun.js builds a multipart request with the FormData that Node.js 18+ provides. +const client = new Mailgun(FormData).client({ + username: "api", + key: requireEnv("MAILGUN_API_KEY"), + // Domains in Mailgun's EU region use https://api.eu.mailgun.net. + url: process.env.MAILGUN_URL ?? "https://api.mailgun.net", +}); +const { from, to, subject, html, text } = deliveryMessage(); +const result = await client.messages.create(requireEnv("MAILGUN_DOMAIN"), { + from, + to: [to], + subject, + html, + text, +}); +console.info("Mailgun accepted:", result.id); +``` + +```bash +pnpm --filter @unlayer/content-workflows-example send:mailgun +``` + +Expected: `Mailgun accepted: `. + +## Azure Communication Services Email + +Install in your own project: `npm install @azure/communication-email`. The workspace example already declares this dependency. + +Set `AZURE_COMMUNICATION_CONNECTION_STRING` to the connection string of a Communication Services resource with an email domain connected to it, and use a sender address from that domain (for example `DoNotReply@`) as `EMAIL_FROM`. To use Microsoft Entra ID instead of the access key, pass the endpoint and a credential from `@azure/identity`: `new EmailClient(endpoint, new DefaultAzureCredential())`. `beginSend()` queues the message; `pollUntilDone()` waits until Azure reports the send operation's final status. [Azure email quickstart](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/email/send-email?pivots=programming-language-javascript). + +```ts +import { EmailClient } from "@azure/communication-email"; +import { deliveryMessage, requireEnv } from "../message"; + +const client = new EmailClient(requireEnv("AZURE_COMMUNICATION_CONNECTION_STRING")); +const { from, to, subject, html, text } = deliveryMessage(); +// beginSend queues the message and returns a poller for the send operation. +const poller = await client.beginSend({ + senderAddress: from, + recipients: { to: [{ address: to }] }, + content: { subject, html, plainText: text }, +}); +const result = await poller.pollUntilDone(); +if (result.status !== "Succeeded") { + throw new Error(`Azure Communication Services: ${result.error?.message ?? result.status}`); +} +console.info("Azure accepted:", result.id); +``` + +```bash +pnpm --filter @unlayer/content-workflows-example send:azure +``` + +Expected: `Azure accepted: `. + +## MailerSend + +Install in your own project: `npm install mailersend`. The workspace example already declares this dependency. + +Set `MAILERSEND_API_KEY` to an API token with email access, and send from a verified domain. MailerSend answers `202 Accepted` with the message ID in the `x-message-id` header. On an HTTP error the SDK rejects with an object holding `statusCode`, `headers`, and `body`, not with an `Error`. [MailerSend Node.js SDK](https://github.com/mailersend/mailersend-nodejs). + +```ts +import { EmailParams, MailerSend, Recipient, Sender } from "mailersend"; +import { deliveryMessage, requireEnv } from "../message"; + +const client = new MailerSend({ apiKey: requireEnv("MAILERSEND_API_KEY") }); +const { from, to, subject, html, text } = deliveryMessage(); +const params = new EmailParams() + .setFrom(new Sender(from)) + .setTo([new Recipient(to)]) + .setSubject(subject) + .setHtml(html) + .setText(text); +const response = await client.email.send(params); +console.info("MailerSend accepted:", response.statusCode, response.headers["x-message-id"]); +``` + +```bash +pnpm --filter @unlayer/content-workflows-example send:mailersend +``` + +Expected: `MailerSend accepted: 202 `. + +## Scaleway Transactional Email + +Install in your own project: `npm install @scaleway/sdk-tem @scaleway/sdk-client`. The workspace example already declares these dependencies. + +Set `SCW_ACCESS_KEY`, `SCW_SECRET_KEY` (an IAM API key allowed to send email), and `SCW_DEFAULT_PROJECT_ID` (the Project that owns your checked domain). Transactional Email runs in the `fr-par` region. The package exports the API as `Temv1alpha1`; the response lists the emails it created. [Scaleway Transactional Email](https://www.scaleway.com/en/docs/transactional-email/), [API reference](https://www.scaleway.com/en/developers/api/transactional-email/). + +```ts +import { createClient } from "@scaleway/sdk-client"; +import { Temv1alpha1 } from "@scaleway/sdk-tem"; +import { deliveryMessage, requireEnv } from "../message"; + +const client = createClient({ + accessKey: requireEnv("SCW_ACCESS_KEY"), + secretKey: requireEnv("SCW_SECRET_KEY"), + defaultProjectId: requireEnv("SCW_DEFAULT_PROJECT_ID"), + // Transactional Email runs in the fr-par region. + defaultRegion: "fr-par", +}); +const tem = new Temv1alpha1.API(client); +const { from, to, subject, html, text } = deliveryMessage(); +const { emails } = await tem.createEmail({ + from: { email: from }, + to: [{ email: to }], + subject, + html, + text, +}); +console.info("Scaleway accepted:", emails.map((email) => email.id).join(", ")); +``` + +```bash +pnpm --filter @unlayer/content-workflows-example send:scaleway +``` + +Expected: `Scaleway accepted: `. + +## Plunk + +No package to install: the `@plunk/node` package on npm is deprecated, so this script calls Plunk's REST API with the `fetch` built into Node.js 18 and later. + +Set `PLUNK_SECRET_KEY` to your project's secret key (`sk_...`; the public `pk_` key can't send) and send from a verified domain. The API takes the HTML as `body` and has no plain-text field: Plunk generates the text part from the HTML. Each send also creates or updates a contact for the recipient. [Plunk send API](https://docs.useplunk.com/api-reference/public-api/sendEmail). + +```ts +import { deliveryMessage, requireEnv } from "../message"; + +// The Plunk npm package is deprecated, so this calls Plunk's REST API directly. +// Plunk derives the plain-text part from `body`; the API has no text field. +type PlunkResponse = + | { success: true; data: { emails: { email: string }[] } } + | { success: false; error: { code: string; message: string } }; + +const { from, to, subject, html } = deliveryMessage(); +const response = await fetch("https://next-api.useplunk.com/v1/send", { + method: "POST", + headers: { + Authorization: `Bearer ${requireEnv("PLUNK_SECRET_KEY")}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ from, to, subject, body: html }), +}); +const result = (await response.json()) as PlunkResponse; +if (!result.success) throw new Error(`Plunk ${response.status}: ${result.error.message}`); +console.info("Plunk accepted:", result.data.emails[0].email); +``` + +```bash +pnpm --filter @unlayer/content-workflows-example send:plunk +``` + +Expected: `Plunk accepted: `. + +## Lettermint + +Install in your own project: `npm install lettermint`. The workspace example already declares this dependency. + +Set `LETTERMINT_PROJECT_TOKEN` to a project sending token (`lm_...`); a team token (`lm_team_...`) can't send. Send from a domain verified in Lettermint. [Lettermint Node.js SDK](https://github.com/lettermint/lettermint-node). + +```ts +import { Lettermint } from "lettermint"; +import { deliveryMessage, requireEnv } from "../message"; + +const client = new Lettermint({ sendingToken: requireEnv("LETTERMINT_PROJECT_TOKEN") }); +const { from, to, subject, html, text } = deliveryMessage(); +const result = await client.emails.send({ from, to: [to], subject, html, text }); +console.info("Lettermint accepted:", result.message_id); +``` + +```bash +pnpm --filter @unlayer/content-workflows-example send:lettermint +``` + +Expected: `Lettermint accepted: `. + +## Brevo + +Install in your own project: `npm install @getbrevo/brevo`. The workspace example already declares this dependency. The snippet uses the `BrevoClient` API that `@getbrevo/brevo` has had since version 4; version 3 and earlier used `TransactionalEmailsApi` instead. + +Set `BREVO_API_KEY` to a Brevo API key and send from a verified sender or domain. [Brevo Node.js SDK](https://github.com/getbrevo/brevo-node), [transactional email guide](https://developers.brevo.com/docs/send-a-transactional-email). + +```ts +import { BrevoClient } from "@getbrevo/brevo"; +import { deliveryMessage, requireEnv } from "../message"; + +const client = new BrevoClient({ apiKey: requireEnv("BREVO_API_KEY") }); +const { from, to, subject, html, text } = deliveryMessage(); +const result = await client.transactionalEmails.sendTransacEmail({ + sender: { email: from }, + to: [{ email: to }], + subject, + htmlContent: html, + textContent: text, +}); +console.info("Brevo accepted:", result.messageId); +``` + +```bash +pnpm --filter @unlayer/content-workflows-example send:brevo +``` + +Expected: `Brevo accepted: `. + +## Inbound + +Install in your own project: `npm install inboundemail`. The workspace example already declares this dependency. The older `@inboundemail/sdk` package is deprecated. + +Set `INBOUND_API_KEY` and send from a domain verified in Inbound. The SDK's `react` option renders React Email components, not Elements; pass the Elements output as `html` and `text`. [Inbound TypeScript SDK](https://github.com/inboundemail/inbound-typescript-sdk), [API docs](https://inbound.new/docs). + +```ts +import Inbound from "inboundemail"; +import { deliveryMessage, requireEnv } from "../message"; + +const client = new Inbound({ apiKey: requireEnv("INBOUND_API_KEY") }); +const { from, to, subject, html, text } = deliveryMessage(); +const result = await client.emails.send({ from, to, subject, html, text }); +console.info("Inbound accepted:", result.id); +``` + +```bash +pnpm --filter @unlayer/content-workflows-example send:inbound +``` + +Expected: `Inbound accepted: `. + +## eusend + +Install in your own project: `npm install @eusend_dev/sdk`. The workspace example already declares this dependency. + +Set `EUSEND_API_KEY`. Keys starting with `eu_test_` exercise the API without delivering; use an `eu_live_` key and a verified domain to deliver. Like Resend, the SDK returns `{ data, error }` instead of throwing. Its `react` option renders React Email components, not Elements; pass the Elements output as `html` and `text`. [eusend Node.js SDK](https://eusend.dev/docs/sdks/nodejs). + +```ts +import { Eusend } from "@eusend_dev/sdk"; +import { deliveryMessage, requireEnv } from "../message"; + +const client = new Eusend(requireEnv("EUSEND_API_KEY")); +const { data, error } = await client.emails.send(deliveryMessage()); +if (error) throw new Error(`eusend: ${error.name} ${error.message}`); +console.info("eusend accepted:", data.id); +``` + +```bash +pnpm --filter @unlayer/content-workflows-example send:eusend +``` + +Expected: `eusend accepted: `. + For PDF attachments, first generate bytes with the [Playwright guide](./react-to-pdf.md), then follow your provider's attachment API and size limits. Design JSON is for the [visual editor](./visual-editing.md), not an email body. diff --git a/packages/react/docs/i18n.md b/packages/react/docs/i18n.md new file mode 100644 index 00000000..9040c5ba --- /dev/null +++ b/packages/react/docs/i18n.md @@ -0,0 +1,239 @@ +# Translate React emails with react-intl, react-i18next, or next-intl + +Elements has no translation system of its own: use the i18n library your application already has. This guide renders one shipping notification in English and Arabic with [react-intl](#react-intl), [react-i18next](#react-i18next), and [next-intl](#next-intl). Every snippet is a file in the [i18n example](../../../examples/i18n), and its test renders both locales with each library. + +```bash +pnpm install +pnpm build +pnpm --filter @unlayer/i18n-example test +pnpm --filter @unlayer/i18n-example render +``` + +`render` writes `examples/i18n/output/shipped.en.html` and `shipped.ar.html` (and the plain text of each). Open the Arabic file to see the right-to-left layout. + +## The rules + +1. **Load messages, then render.** `renderToHtml()`, `renderToPlainText()`, and `renderToJson()` are synchronous. Read message files, initialize the i18n instance, and fetch data before you call them. A template that suspends while its messages load (react-i18next's `useTranslation()` does) makes the renderer throw `async and suspending templates need "@unlayer/react-elements/async"'s renderers, or their data loaded first`; a component further down that suspends or is `async` is left out of the email, with a message naming it. Don't `await` translations inside a component. (The [async renderers](./async.md) render templates that load data, but messages loaded first keep the render synchronous, and the email can't go out with keys in it.) +2. **Providers and hooks work.** Wrap the template in the library's provider and call its hook in your components: Elements renders the template once with React, so context reaches every component, including the one that returns ``. A provider written as a class component can't be looked through: the root's `lang` and `textDirection` are then ignored, with a warning. +3. **Give blocks strings or message components.** Call `intl.formatMessage()` or `t()` and pass the string to `Heading`, `Paragraph`, or `Button`, as the templates below do. Their children can also be `` or ``, which render with the template's context, formatting tags included. Props such as `previewText` and `href` take strings only. +4. **Set the language and direction on the root.** `` writes `` and marks each text block `dir="rtl"`. The design JSON keeps `textDirection` but not `lang`; options passed to `renderToHtml()` override both props. +5. **Text follows the direction.** In a right-to-left email, `Heading` and `Paragraph` default to `textAlign="right"`, so Arabic or Hebrew text starts at the right edge. Pass `textAlign` to align a block otherwise. (Elements 0.2 left-aligned them in every direction.) +6. **Translate the subject and preview text too.** The subject goes to your email provider, not into the HTML; the inbox preview is the root's `previewText`. +7. **Escape once.** Elements escapes text passed as children, so turn off escaping in the i18n library (i18next's `escapeValue: false`) to avoid `&lt;`. Values interpolated into a `Paragraph`'s `html` prop are raw markup: escape them yourself, as the [invoice template](./one-template-email-web-pdf.md#define-the-shared-react-template) does. + +## Messages and direction + +The example keeps one JSON file per locale in [messages/](../../../examples/i18n/messages), in ICU syntax, and all three libraries read the same files: + +```json +{ + "shipped": { + "subject": "تم شحن طلبك {orderId}", + "preview": "طلبك في الطريق إليك.", + "heading": "تم شحن طلبك", + "greeting": "مرحبًا {name}، طلبك {orderId} في الطريق إليك.", + "cta": "تتبّع شحنتك" + } +} +``` + +[src/locales.ts](../../../examples/i18n/src/locales.ts) loads them and maps a locale to its direction: + +```ts +import { readFile } from "node:fs/promises"; + +export const locales = ["en", "ar"] as const; +export type Locale = (typeof locales)[number]; + +/** One file per locale in messages/, nested by email, with ICU placeholders: "Hi {name}". */ +export interface Messages { + shipped: Record<"subject" | "preview" | "heading" | "greeting" | "cta", string>; +} + +/** Elements renders synchronously, so load a locale's messages before rendering. */ +export async function loadMessages(locale: Locale): Promise { + return JSON.parse(await readFile(new URL(`../messages/${locale}.json`, import.meta.url), "utf8")); +} + +const RTL_LANGUAGES = new Set(["ar", "fa", "he", "ur"]); + +/** The value for the root's `textDirection` prop. */ +export function directionOf(locale: string): "ltr" | "rtl" { + return RTL_LANGUAGES.has(new Intl.Locale(locale).language) ? "rtl" : "ltr"; +} + +export interface Order { + id: string; + customer: string; + trackingUrl: string; +} + +export const sampleOrder: Order = { + id: "A-1042", + customer: "Ada", + trackingUrl: "https://example.com/orders/A-1042", +}; +``` + +## react-intl + +`createIntl()` makes the formatter for the subject, and `RawIntlProvider` gives the same one to the template, whose components call `useIntl()`. `` works as well. [src/react-intl.tsx](../../../examples/i18n/src/react-intl.tsx): + +```tsx +import { + Button, Column, ColumnLayouts, Email, Heading, Paragraph, Row, + renderToHtml, renderToPlainText, +} from "@unlayer/react-elements"; +import { createIntl, RawIntlProvider, useIntl } from "react-intl"; +import { directionOf, type Locale, type Messages, type Order } from "./locales"; + +function ShippedEmail({ order }: { order: Order }) { + const intl = useIntl(); + const values = { name: order.customer, orderId: order.id }; + const direction = directionOf(intl.locale); + return ( + + + + {intl.formatMessage({ id: "shipped.heading" })} + {intl.formatMessage({ id: "shipped.greeting" }, values)} + + + + + ); +} + +export function renderShippedEmail(locale: Locale, messages: Messages, order: Order) { + // react-intl looks messages up by flat ids: { "shipped.heading": "…" }. + const flat = Object.fromEntries( + Object.entries(messages.shipped).map(([key, message]) => [`shipped.${key}`, message]), + ); + const intl = createIntl({ locale, messages: flat }); + const email = ( + + + + ); + return { + subject: intl.formatMessage({ id: "shipped.subject" }, { orderId: order.id }), + html: renderToHtml(email), + text: renderToPlainText(email), + }; +} +``` + +## react-i18next + +Create an i18next instance per locale and `await` its `init()`, so `useTranslation()` has its messages when the template renders. `i18n.dir()` gives the direction of the instance's language. [src/react-i18next.tsx](../../../examples/i18n/src/react-i18next.tsx): + +```tsx +import { + Button, Column, ColumnLayouts, Email, Heading, Paragraph, Row, + renderToHtml, renderToPlainText, +} from "@unlayer/react-elements"; +import i18next, { type i18n } from "i18next"; +import { I18nextProvider, useTranslation } from "react-i18next"; +import type { Locale, Messages, Order } from "./locales"; + +/** An i18next instance per locale, initialized before anything renders with it. */ +export async function createI18n(locale: Locale, messages: Messages): Promise { + const instance = i18next.createInstance(); + await instance.init({ + lng: locale, + resources: { [locale]: { translation: messages } }, + interpolation: { + // The shared message files use {name}; i18next's default is {{name}}. + prefix: "{", + suffix: "}", + // Elements escapes text children already. + escapeValue: false, + }, + }); + return instance; +} + +function ShippedEmail({ order }: { order: Order }) { + const { t, i18n } = useTranslation(); + const direction = i18n.dir(); + return ( + + + + {t("shipped.heading")} + {t("shipped.greeting", { name: order.customer, orderId: order.id })} + + + + + ); +} + +export function renderShippedEmail(i18n: i18n, order: Order) { + const email = ( + + + + ); + return { + subject: i18n.t("shipped.subject", { orderId: order.id }), + html: renderToHtml(email), + text: renderToPlainText(email), + }; +} +``` + +Rendering with an instance that hasn't loaded its messages throws instead of sending keys or an empty email; the example's test checks this. Don't work around it with `useSuspense: false`: the template would render before the messages are there. + +## next-intl + +next-intl's request-scoped APIs need a Next.js request. Outside one (a script, a job worker, a test), use `createTranslator()`, which needs only a locale and messages. next-intl's Server Component entry exports it too, so the same template can run in a Route Handler. The translator is typed by the messages: a missing key is a type error. [src/next-intl.tsx](../../../examples/i18n/src/next-intl.tsx): + +```tsx +import { + Button, Column, ColumnLayouts, Email, Heading, Paragraph, Row, + renderToHtml, renderToPlainText, +} from "@unlayer/react-elements"; +import { createTranslator } from "next-intl"; +import { directionOf, type Locale, type Messages, type Order } from "./locales"; + +type ShippedTranslator = ReturnType>; + +function shippedEmail(t: ShippedTranslator, locale: Locale, order: Order) { + const direction = directionOf(locale); + return ( + + + + {t("heading")} + {t("greeting", { name: order.customer, orderId: order.id })} + + + + + ); +} + +export function renderShippedEmail(locale: Locale, messages: Messages, order: Order) { + // createTranslator needs no request: it works in scripts, job workers and Next.js server code alike. + const t = createTranslator({ locale, messages, namespace: "shipped" }); + const email = shippedEmail(t, locale, order); + return { + subject: t("subject", { orderId: order.id }), + html: renderToHtml(email), + text: renderToPlainText(email), + }; +} +``` + +Inside a Next.js request (a Route Handler, a Server Action, or a Server Component), you can instead `await getTranslations({ locale, namespace: "shipped" })` from `next-intl/server`, with your next-intl request configuration, and pass the result to the template. Await it before rendering. Outside Next.js server code (in a plain Node.js script, for example) it throws an error saying `getTranslations` is not supported in Client Components. The example's test covers `createTranslator()` only. next-intl lists `next` as a peer dependency, so install it even where you only use `createTranslator()`. + +## Related guides + +- [Transactional email](./transactional-email.md): render HTML and plain text for a provider. +- [Email providers](./email-providers.md): send the translated subject, HTML, and text. +- [Visual editing](./visual-editing.md): when opening a right-to-left design in the editor, also call `unlayer.setTextDirection("rtl")`. diff --git a/packages/react/docs/monorepo.md b/packages/react/docs/monorepo.md new file mode 100644 index 00000000..42190887 --- /dev/null +++ b/packages/react/docs/monorepo.md @@ -0,0 +1,260 @@ +# Use Elements in a monorepo with npm, pnpm, Yarn, or Bun + +Keep your email templates in one workspace package, render them there, and import render functions from your apps. This guide covers where the dependencies go, how apps consume the package, and the two problems monorepos add: a package that can't find Elements or React, and two copies of React. The pnpm, npm, Yarn 4, and Bun setups below were each installed and run with `@unlayer/react-elements` 0.2.1 and React 19.2.8, and an app rendered the package's templates. The preview script and the app's render below were run with 0.3.0. + +## Layout + +```text +acme/ +├── package.json +├── pnpm-workspace.yaml # pnpm only +├── packages/ +│ └── emails/ # @acme/emails +│ ├── package.json +│ ├── emails/ +│ │ ├── welcome.tsx # one template per file +│ │ └── _components/ # shared parts +│ │ └── footer.tsx +│ ├── scripts/ +│ │ └── preview.ts # writes each template's HTML to open in a browser +│ └── src/ +│ └── index.ts # what apps import +└── apps/ + └── api/ # depends on @acme/emails + └── package.json +``` + +## Where the dependencies go + +- **The emails package** declares `@unlayer/react-elements`, `react`, and `react-dom` in its `dependencies`, and `tsx` (for its preview script) in its `devDependencies`. Everything a template imports must be declared there. +- **An app that calls the package's render functions** depends only on the emails package. It doesn't need React. +- **An app that renders the package's components itself** (`renderToHtml()` in a route handler, for example) also declares Elements and React, at exactly the same versions as the emails package. See [one copy of React](#one-copy-of-react). + +A template loads Elements and React as Node resolves any import: from the folder of the file that imports it, up through the `node_modules` folders above it. So the templates render with the copies the emails package resolves, wherever you run a script from, and that package must be able to resolve them. + +## The shared emails package + +`packages/emails/emails/welcome.tsx`, a template (a default export, with `PreviewProps` for the preview script): + +```tsx +import { Button, Column, ColumnLayouts, Email, Heading, Row } from "@unlayer/react-elements"; +import { Footer } from "./_components/footer"; + +export default function Welcome({ name }: { name: string }) { + return ( + + + + Welcome, {name} + + + +