react-email-locale-lab 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # React Email Locale Lab
2
+
3
+ React Email Locale Lab is a proof of concept for previewing React Email templates in multiple languages while editing them.
4
+
5
+ The idea came from an internal need I encountered while working at one of the companies in my career. We needed a faster way to understand how email templates behaved across languages, especially when translated text changed the size, spacing or structure of a layout.
6
+
7
+ This project exists to test the technical and product viability of that workflow. It is not yet intended to be a production localization system or a finished commercial library.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add -D react-email-locale-lab
13
+ ```
14
+
15
+ The package expects React 19 and React DOM 19 as peer dependencies.
16
+
17
+ Create a configuration in the consuming project:
18
+
19
+ ```tsx
20
+ import {
21
+ browserTranslatorProvider,
22
+ defineEmailLab,
23
+ EmailLabApp,
24
+ } from 'react-email-locale-lab';
25
+ import 'react-email-locale-lab/styles.css';
26
+
27
+ const config = defineEmailLab({
28
+ routeBasePath: '/preview',
29
+ sourceLocale: { code: 'en', label: 'English' },
30
+ locales: [{ code: 'de', label: 'Deutsch' }],
31
+ provider: browserTranslatorProvider(),
32
+ templates: {
33
+ welcome: { name: 'Welcome', render: () => <WelcomeEmail /> },
34
+ },
35
+ });
36
+
37
+ export const App = () => <EmailLabApp config={config} />;
38
+ ```
39
+
40
+ Import `react-email-locale-lab/styles.css` once in the consuming application. During local development of this repository, `pnpm pack` can be used to create an installable tarball before publishing to npm.
41
+
42
+ ## Run
43
+
44
+ ```bash
45
+ pnpm install
46
+ pnpm dev
47
+ ```
48
+
49
+ Open `http://localhost:4173/preview/welcome`, select up to three languages, then edit `src/emails/welcome.tsx`. Vite refreshes the source and the selected locale cards transition through `stale → translating → ready`.
50
+
51
+ ## Configuration interface
52
+
53
+ `src/email-lab.config.tsx` defines only the source locale, available target locales, templates, and translation provider. There are no manually maintained translations. The user selects zero to three languages in the UI; translation is not initialized during app startup.
54
+
55
+ ```tsx
56
+ export default defineEmailLab({
57
+ routeBasePath: '/preview',
58
+ sourceLocale: { code: 'en', label: 'English' },
59
+ locales: [
60
+ { code: 'de', label: 'Deutsch' },
61
+ { code: 'pt-BR', translationCode: 'pt', label: 'Português (Brasil)' },
62
+ ],
63
+ provider: browserTranslatorProvider(),
64
+ templates: {
65
+ welcome: { name: 'Welcome', render: () => <WelcomeEmail /> },
66
+ },
67
+ });
68
+ ```
69
+
70
+ ## Template routes
71
+
72
+ Every configured template has a stable preview route:
73
+
74
+ ```text
75
+ /preview/welcome
76
+ /preview/passwordReset?langs=pt-BR,de
77
+ ```
78
+
79
+ Set `routeBasePath` to mount the lab elsewhere, such as `/email-preview`. Selecting a template updates the pathname, selected languages stay in `?langs=`, and browser back/forward navigation restores both template and locales. The host development server must use SPA fallback so these routes return the app entry point; Vite does this automatically in development.
80
+
81
+ ## Languages
82
+
83
+ Locale identifiers use BCP 47 codes. `code` controls selection, URL state and the rendered HTML `lang` attribute. Optional `translationCode` lets a provider use a broader supported language while preserving a regional application locale—for example, `{ code: 'pt-BR', translationCode: 'pt' }`.
84
+
85
+ The included Chrome provider supports these language codes in the current Chrome implementation:
86
+
87
+ ```text
88
+ ar bg bn cs da de el en es fi fr he hi hr hu id it ja kn ko lt mr
89
+ nl no pl pt ro ru sk sl sv ta te th tr uk vi zh zh-Hant
90
+ ```
91
+
92
+ The list is provider-specific and may change. The provider checks every source/target pair with `Translator.availability()` at runtime. Chrome's API currently runs on desktop, downloads language packs on demand and is unavailable on mobile. See the [official Translator API language list](https://developer.chrome.com/docs/ai/translator-api#supported-languages).
93
+
94
+ The provider receives extracted text nodes and translatable attributes (`alt`, `title`, `aria-label`). When source copy changes, every target preview is marked stale and new or changed messages are automatically translated again. Unchanged messages are reused from the in-memory cache.
95
+
96
+ The included provider uses the browser's built-in Translator API. Language packs are lazy-loaded per selected pair, and translated messages are cached by source text and locale. The source template does not leave the browser. In a browser without this API, the preview reports an actionable error instead of downloading a large JavaScript model or freezing startup.
97
+
98
+ ## Current scope
99
+
100
+ React Email Locale Lab is currently a POC for validating the developer experience, not a production localization system. It automatically translates rendered text for preview only. Future iterations may protect placeholders, expose language-pack progress and offer an explicitly configured server provider for teams whose browsers do not support on-device translation.
package/dist/App.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { EmailLabConfig } from './core/types';
2
+ export declare const EmailLabApp: ({ config }: {
3
+ config: EmailLabConfig;
4
+ }) => import("react").JSX.Element;
5
+ //# sourceMappingURL=App.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,eAAO,MAAM,WAAW,GAAI,YAAY;IAAE,MAAM,EAAE,cAAc,CAAA;CAAE,gCAsDjE,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { Locale } from '../../core/types';
2
+ import type { PreviewState } from '../types';
3
+ type PreviewCardProps = {
4
+ locale: Locale;
5
+ preview: PreviewState;
6
+ sourceRevision: string;
7
+ onRefresh: () => void;
8
+ };
9
+ export declare const PreviewCard: ({ locale, preview, sourceRevision, onRefresh }: PreviewCardProps) => import("react").JSX.Element;
10
+ export {};
11
+ //# sourceMappingURL=PreviewCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PreviewCard.d.ts","sourceRoot":"","sources":["../../../src/app/components/PreviewCard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C,KAAK,gBAAgB,GAAG;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,YAAY,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,gDAAgD,gBAAgB,gCAY3F,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { EmailLabConfig } from '../../core/types';
2
+ import type { PreviewState } from '../types';
3
+ export declare const useEmailLab: (config: EmailLabConfig) => {
4
+ activeLocaleCodes: string[];
5
+ activeLocales: import("../..").Locale[];
6
+ previewLocales: import("../..").Locale[];
7
+ previews: Record<string, PreviewState>;
8
+ refreshPreviews: () => void;
9
+ selectTemplate: (templateId: string) => void;
10
+ selectedTemplateId: string;
11
+ sourceHtml: string;
12
+ sourceRevision: string;
13
+ templateIds: string[];
14
+ toggleLocale: (localeCode: string) => void;
15
+ };
16
+ //# sourceMappingURL=useEmailLab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useEmailLab.d.ts","sourceRoot":"","sources":["../../../src/app/hooks/useEmailLab.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAK7C,eAAO,MAAM,WAAW,GAAI,QAAQ,cAAc;;;;;;iCAsFZ,MAAM;;;;;+BAKR,MAAM;CAqBzC,CAAC"}
@@ -0,0 +1,8 @@
1
+ export type PreviewStatus = 'source' | 'stale' | 'translating' | 'ready' | 'error';
2
+ export type PreviewState = {
3
+ html: string;
4
+ status: PreviewStatus;
5
+ revision?: string;
6
+ error?: string;
7
+ };
8
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/app/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,OAAO,GAAG,aAAa,GAAG,OAAO,GAAG,OAAO,CAAC;AAEnF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC"}
@@ -0,0 +1,6 @@
1
+ export declare const normalizeRouteBasePath: (value?: string) => string;
2
+ export declare const templateIdFromUrl: (url: URL, templateIds: string[], routeBasePath?: string) => string;
3
+ export declare const localeCodesFromUrl: (url: URL, limit?: number) => string[];
4
+ export declare const urlForTemplate: (url: URL, templateId: string, routeBasePath?: string) => URL;
5
+ export declare const urlForLocales: (url: URL, localeCodes: string[]) => URL;
6
+ //# sourceMappingURL=url-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-state.d.ts","sourceRoot":"","sources":["../../../src/app/utils/url-state.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,sBAAsB,GAAI,cAA+B,WAGrE,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,KAAK,GAAG,EAAE,aAAa,MAAM,EAAE,EAAE,gBAAgB,MAAM,WAOxF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,KAAK,GAAG,EAAE,cAAS,aAC2B,CAAC;AAElF,eAAO,MAAM,cAAc,GAAI,KAAK,GAAG,EAAE,YAAY,MAAM,EAAE,gBAAgB,MAAM,QAKlF,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,KAAK,GAAG,EAAE,aAAa,MAAM,EAAE,QAK5D,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { TranslationProvider } from './types';
2
+ type BrowserTranslator = {
3
+ translate: (text: string) => Promise<string>;
4
+ destroy?: () => void;
5
+ };
6
+ type TranslatorFactory = {
7
+ availability: (options: {
8
+ sourceLanguage: string;
9
+ targetLanguage: string;
10
+ }) => Promise<string>;
11
+ create: (options: {
12
+ sourceLanguage: string;
13
+ targetLanguage: string;
14
+ }) => Promise<BrowserTranslator>;
15
+ };
16
+ declare global {
17
+ interface Window {
18
+ Translator?: TranslatorFactory;
19
+ }
20
+ }
21
+ export declare const browserTranslatorProvider: () => TranslationProvider;
22
+ export {};
23
+ //# sourceMappingURL=browser-translator-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-translator-provider.d.ts","sourceRoot":"","sources":["../../src/core/browser-translator-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEnD,KAAK,iBAAiB,GAAG;IAAE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC;AAChG,KAAK,iBAAiB,GAAG;IACvB,YAAY,EAAE,CAAC,OAAO,EAAE;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/F,MAAM,EAAE,CAAC,OAAO,EAAE;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACrG,CAAC;AAEF,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QAAG,UAAU,CAAC,EAAE,iBAAiB,CAAA;KAAE;CACpD;AAED,eAAO,MAAM,yBAAyB,QAAO,mBAwD5C,CAAC"}
@@ -0,0 +1,9 @@
1
+ export type MessageSlot = {
2
+ value: string;
3
+ apply: (translated: string) => void;
4
+ };
5
+ export declare const collectMessages: (document: Document) => MessageSlot[];
6
+ export declare const localizeHtml: (sourceHtml: string, sourceLocale: string, targetLocale: string, translate: (texts: string[]) => Promise<string[]>) => Promise<string>;
7
+ export declare const fingerprint: (value: string) => Promise<string>;
8
+ export declare const extractPreheader: (html: string) => string | undefined;
9
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/core/html.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,CAAC;AAGjF,eAAO,MAAM,eAAe,GAAI,UAAU,QAAQ,KAAG,WAAW,EAwB/D,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,YAAY,MAAM,EAClB,cAAc,MAAM,EACpB,cAAc,MAAM,EACpB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,KAChD,OAAO,CAAC,MAAM,CAQhB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAU,OAAO,MAAM,KAAG,OAAO,CAAC,MAAM,CAI/D,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,MAAM,GAAG,SAOxD,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { ReactElement } from 'react';
2
+ export type Locale = {
3
+ code: string;
4
+ label: string;
5
+ translationCode?: string;
6
+ };
7
+ export type EmailTemplate = {
8
+ name: string;
9
+ render: () => ReactElement;
10
+ };
11
+ export type TranslationRequest = {
12
+ texts: string[];
13
+ sourceLocale: string;
14
+ targetLocale: string;
15
+ };
16
+ export type TranslationProvider = {
17
+ name: string;
18
+ translate: (request: TranslationRequest) => Promise<string[]>;
19
+ };
20
+ export type EmailLabConfig = {
21
+ sourceLocale: Locale;
22
+ locales: Locale[];
23
+ templates: Record<string, EmailTemplate>;
24
+ provider: TranslationProvider;
25
+ routeBasePath?: string;
26
+ };
27
+ export declare const defineEmailLab: (config: EmailLabConfig) => EmailLabConfig;
28
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAE1C,MAAM,MAAM,MAAM,GAAG;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AACF,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,YAAY,CAAA;CAAE,CAAC;AACzE,MAAM,MAAM,kBAAkB,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC;AACjG,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC/D,CAAC;AACF,MAAM,MAAM,cAAc,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACzC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,QAAQ,cAAc,KAAG,cAAwB,CAAC"}
@@ -0,0 +1,6 @@
1
+ import './styles.css';
2
+ export { EmailLabApp } from './App';
3
+ export { browserTranslatorProvider } from './core/browser-translator-provider';
4
+ export { defineEmailLab } from './core/types';
5
+ export type { EmailLabConfig, EmailTemplate, Locale, TranslationProvider, TranslationRequest, } from './core/types';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAC;AAEtB,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,YAAY,EACV,cAAc,EACd,aAAa,EACb,MAAM,EACN,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,244 @@
1
+ import { jsxs as d, jsx as l } from "react/jsx-runtime";
2
+ import { useMemo as A, useState as b, useEffect as T } from "react";
3
+ import { renderToStaticMarkup as I } from "react-dom/server";
4
+ const M = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").test(e), k = (e) => {
5
+ const t = [], a = e.createTreeWalker(e.body, NodeFilter.SHOW_TEXT);
6
+ let n = a.nextNode();
7
+ for (; n; ) {
8
+ const s = n, r = s.parentElement, c = s.data.trim();
9
+ if (r && !["STYLE", "SCRIPT"].includes(r.tagName) && $(c)) {
10
+ const o = s.data.match(/^\s*/)?.[0] ?? "", u = s.data.match(/\s*$/)?.[0] ?? "";
11
+ t.push({ value: c, apply: (w) => {
12
+ s.data = `${o}${w}${u}`;
13
+ } });
14
+ }
15
+ n = a.nextNode();
16
+ }
17
+ for (const s of e.body.querySelectorAll("*"))
18
+ for (const r of M) {
19
+ const c = s.getAttribute(r)?.trim();
20
+ c && $(c) && t.push({ value: c, apply: (o) => s.setAttribute(r, o) });
21
+ }
22
+ return t;
23
+ }, j = async (e, t, a, n) => {
24
+ if (a === t) return e;
25
+ const s = new DOMParser().parseFromString(e, "text/html");
26
+ s.documentElement.lang = a;
27
+ const r = k(s), c = await n(r.map((o) => o.value));
28
+ return r.forEach((o, u) => o.apply(c[u] ?? o.value)), `<!doctype html>${s.documentElement.outerHTML}`;
29
+ }, U = async (e) => {
30
+ const t = new TextEncoder().encode(e), a = await crypto.subtle.digest("SHA-256", t);
31
+ return Array.from(new Uint8Array(a).slice(0, 6), (n) => n.toString(16).padStart(2, "0")).join("");
32
+ }, H = (e) => {
33
+ const t = new DOMParser().parseFromString(e, "text/html");
34
+ return Array.from(t.body.querySelectorAll("[style]")).find(
35
+ (s) => s.style.display === "none" && s.textContent?.trim()
36
+ )?.textContent?.replace(/[\u00A0\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g, "").trim() || void 0;
37
+ }, O = ({ locale: e, preview: t, sourceRevision: a, onRefresh: n }) => /* @__PURE__ */ d("article", { className: "preview-card", children: [
38
+ /* @__PURE__ */ d("header", { className: "preview-header", children: [
39
+ /* @__PURE__ */ d("div", { children: [
40
+ /* @__PURE__ */ l("strong", { children: e.label }),
41
+ /* @__PURE__ */ l("span", { children: e.code })
42
+ ] }),
43
+ /* @__PURE__ */ d("div", { className: `status status-${t.status}`, children: [
44
+ /* @__PURE__ */ l("i", {}),
45
+ t.status
46
+ ] })
47
+ ] }),
48
+ /* @__PURE__ */ d("div", { className: "revision", children: [
49
+ "source ",
50
+ a || "…",
51
+ " · preview ",
52
+ t.revision ?? "pending"
53
+ ] }),
54
+ /* @__PURE__ */ d("div", { className: "preheader", children: [
55
+ /* @__PURE__ */ l("span", { children: "Preheader" }),
56
+ /* @__PURE__ */ l("strong", { children: H(t.html) ?? "Not set" })
57
+ ] }),
58
+ /* @__PURE__ */ l("iframe", { title: `${e.label} preview`, srcDoc: t.html, sandbox: "allow-popups allow-popups-to-escape-sandbox" }),
59
+ t.error && /* @__PURE__ */ l("div", { className: "preview-error", children: t.error }),
60
+ t.status !== "source" && /* @__PURE__ */ l("button", { className: "refresh", onClick: n, children: "Regenerate translation" })
61
+ ] }), x = "/preview", F = (e = x) => `/${e}`.replace(/\/+/g, "/").replace(/\/$/, "") || x, R = (e, t, a) => {
62
+ const n = F(a), r = (e.pathname.startsWith(`${n}/`) ? decodeURIComponent(e.pathname.slice(n.length + 1)) : void 0) || e.searchParams.get("template") || void 0;
63
+ return r && t.includes(r) ? r : t[0];
64
+ }, C = (e, t = 3) => e.searchParams.get("langs")?.split(",").filter(Boolean).slice(0, t) ?? [], B = (e, t, a) => {
65
+ const n = new URL(e);
66
+ return n.pathname = `${F(a)}/${encodeURIComponent(t)}`, n.searchParams.delete("template"), n;
67
+ }, _ = (e, t) => {
68
+ const a = new URL(e);
69
+ return t.length ? a.searchParams.set("langs", t.join(",")) : a.searchParams.delete("langs"), a;
70
+ }, S = 3, z = (e) => {
71
+ const t = A(() => Object.keys(e.templates), [e.templates]), [a, n] = b(() => R(new URL(window.location.href), t, e.routeBasePath)), [s, r] = b(() => C(new URL(window.location.href), S)), [c, o] = b({}), [u, w] = b(""), [g, y] = b(0), L = e.templates[a] ?? e.templates[t[0]], h = A(() => `<!doctype html>${I(L.render())}`, [L]), P = e.locales.filter((i) => s.includes(i.code)), N = [e.sourceLocale, ...P];
72
+ return T(() => {
73
+ const i = R(new URL(window.location.href), t, e.routeBasePath);
74
+ window.history.replaceState({}, "", B(new URL(window.location.href), i, e.routeBasePath));
75
+ const v = () => {
76
+ n(R(new URL(window.location.href), t, e.routeBasePath)), r(C(new URL(window.location.href), S));
77
+ };
78
+ return window.addEventListener("popstate", v), () => window.removeEventListener("popstate", v);
79
+ }, [e.routeBasePath, t]), T(() => () => {
80
+ }, []), T(() => {
81
+ let i = !1;
82
+ return U(h).then((v) => {
83
+ i || w(v);
84
+ }), () => {
85
+ i = !0;
86
+ };
87
+ }, [h]), T(() => {
88
+ let i = !1;
89
+ return o((m) => Object.fromEntries(N.map((p) => [
90
+ p.code,
91
+ p.code === e.sourceLocale.code ? { html: h, status: "source" } : { html: m[p.code]?.html ?? h, status: "stale", revision: m[p.code]?.revision }
92
+ ]))), (async () => {
93
+ for (const m of P) {
94
+ if (i) return;
95
+ o((p) => ({
96
+ ...p,
97
+ [m.code]: { ...p[m.code], status: "translating", error: void 0 }
98
+ }));
99
+ try {
100
+ const p = await j(h, e.sourceLocale.code, m.code, (E) => e.provider.translate({
101
+ texts: E,
102
+ sourceLocale: e.sourceLocale.translationCode ?? e.sourceLocale.code,
103
+ targetLocale: m.translationCode ?? m.code
104
+ })), f = await U(h);
105
+ i || o((E) => ({ ...E, [m.code]: { html: p, status: "ready", revision: f } }));
106
+ } catch (p) {
107
+ i || o((f) => ({
108
+ ...f,
109
+ [m.code]: {
110
+ ...f[m.code],
111
+ status: "error",
112
+ error: p instanceof Error ? p.message : String(p)
113
+ }
114
+ }));
115
+ }
116
+ }
117
+ })(), () => {
118
+ i = !0;
119
+ };
120
+ }, [h, g, s.join(",")]), {
121
+ activeLocaleCodes: s,
122
+ activeLocales: P,
123
+ previewLocales: N,
124
+ previews: c,
125
+ refreshPreviews: () => y((i) => i + 1),
126
+ selectTemplate: (i) => {
127
+ n(i), window.history.pushState({}, "", B(new URL(window.location.href), i, e.routeBasePath));
128
+ },
129
+ selectedTemplateId: a,
130
+ sourceHtml: h,
131
+ sourceRevision: u,
132
+ templateIds: t,
133
+ toggleLocale: (i) => {
134
+ const v = s.includes(i) ? s.filter((m) => m !== i) : [...s, i].slice(0, S);
135
+ r(v), window.history.replaceState({}, "", _(new URL(window.location.href), v));
136
+ }
137
+ };
138
+ }, X = ({ config: e }) => {
139
+ const t = z(e);
140
+ return /* @__PURE__ */ d("main", { children: [
141
+ /* @__PURE__ */ d("header", { className: "topbar", children: [
142
+ /* @__PURE__ */ d("div", { children: [
143
+ /* @__PURE__ */ l("p", { className: "eyebrow", children: "React Email Locale Lab" }),
144
+ /* @__PURE__ */ l("h1", { children: "See every language while you build." })
145
+ ] }),
146
+ /* @__PURE__ */ d("div", { className: "controls", children: [
147
+ /* @__PURE__ */ d("label", { children: [
148
+ "Template",
149
+ /* @__PURE__ */ l("select", { value: t.selectedTemplateId, onChange: (a) => t.selectTemplate(a.target.value), children: t.templateIds.map((a) => /* @__PURE__ */ l("option", { value: a, children: e.templates[a].name }, a)) })
150
+ ] }),
151
+ /* @__PURE__ */ d("span", { className: "provider", children: [
152
+ "Provider: ",
153
+ e.provider.name
154
+ ] })
155
+ ] })
156
+ ] }),
157
+ /* @__PURE__ */ d("section", { className: "locale-picker", children: [
158
+ /* @__PURE__ */ d("div", { children: [
159
+ /* @__PURE__ */ l("strong", { children: "Preview languages" }),
160
+ /* @__PURE__ */ l("span", { children: "Select up to three. Language packs load only when selected." })
161
+ ] }),
162
+ /* @__PURE__ */ l("div", { className: "locale-options", children: e.locales.map((a) => /* @__PURE__ */ d(
163
+ "button",
164
+ {
165
+ className: t.activeLocaleCodes.includes(a.code) ? "locale-active" : "",
166
+ disabled: !t.activeLocaleCodes.includes(a.code) && t.activeLocaleCodes.length >= 3,
167
+ onClick: () => t.toggleLocale(a.code),
168
+ children: [
169
+ /* @__PURE__ */ l("i", {}),
170
+ a.label
171
+ ]
172
+ },
173
+ a.code
174
+ )) })
175
+ ] }),
176
+ /* @__PURE__ */ d("section", { className: "notice", children: [
177
+ /* @__PURE__ */ l("span", { className: "live-dot" }),
178
+ "Watching source changes through Vite HMR."
179
+ ] }),
180
+ t.activeLocales.length === 0 && /* @__PURE__ */ d("section", { className: "empty-state", children: [
181
+ /* @__PURE__ */ l("strong", { children: "Select a language to generate a preview." }),
182
+ /* @__PURE__ */ l("span", { children: "The source template renders immediately; translation starts only on demand." })
183
+ ] }),
184
+ /* @__PURE__ */ l("section", { className: "grid", children: t.previewLocales.map((a) => /* @__PURE__ */ l(
185
+ O,
186
+ {
187
+ locale: a,
188
+ preview: t.previews[a.code] ?? { html: t.sourceHtml, status: "stale" },
189
+ sourceRevision: t.sourceRevision,
190
+ onRefresh: t.refreshPreviews
191
+ },
192
+ a.code
193
+ )) })
194
+ ] });
195
+ }, Y = () => {
196
+ const e = /* @__PURE__ */ new Map(), t = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Map(), n = async (r, c) => {
197
+ const o = a.get(r) ?? Promise.resolve();
198
+ let u = () => {
199
+ };
200
+ const w = new Promise((y) => {
201
+ u = y;
202
+ }), g = o.catch(() => {
203
+ }).then(() => w);
204
+ a.set(r, g), await o.catch(() => {
205
+ });
206
+ try {
207
+ return await c();
208
+ } finally {
209
+ u(), a.get(r) === g && a.delete(r);
210
+ }
211
+ }, s = async (r, c) => {
212
+ if (!window.Translator)
213
+ throw new Error("This browser does not support the built-in Translator API. Use a current Chrome build or configure a remote provider.");
214
+ const o = `${r}:${c}`;
215
+ let u = e.get(o);
216
+ if (!u) {
217
+ if (await window.Translator.availability({ sourceLanguage: r, targetLanguage: c }) === "unavailable") throw new Error(`The browser cannot translate ${r} → ${c}.`);
218
+ u = window.Translator.create({ sourceLanguage: r, targetLanguage: c }), e.set(o, u);
219
+ }
220
+ return u;
221
+ };
222
+ return {
223
+ name: "Browser Translator · on-device",
224
+ async translate({ texts: r, sourceLocale: c, targetLocale: o }) {
225
+ if (r.length === 0) return [];
226
+ const u = await s(c, o), w = `${c}:${o}`;
227
+ return n(w, async () => {
228
+ const g = [];
229
+ for (const y of r) {
230
+ const L = `${w}:${y}`;
231
+ let h = t.get(L);
232
+ h || (h = await u.translate(y), t.set(L, h)), g.push(h);
233
+ }
234
+ return g;
235
+ });
236
+ }
237
+ };
238
+ }, J = (e) => e;
239
+ export {
240
+ X as EmailLabApp,
241
+ Y as browserTranslatorProvider,
242
+ J as defineEmailLab
243
+ };
244
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/core/html.ts","../src/app/components/PreviewCard.tsx","../src/app/utils/url-state.ts","../src/app/hooks/useEmailLab.ts","../src/App.tsx","../src/core/browser-translator-provider.ts","../src/core/types.ts"],"sourcesContent":["const TRANSLATABLE_ATTRIBUTES = ['alt', 'title', 'aria-label'];\n\nexport type MessageSlot = { value: string; apply: (translated: string) => void };\nconst isMeaningful = (value: string) => /\\p{L}/u.test(value);\n\nexport const collectMessages = (document: Document): MessageSlot[] => {\n const slots: MessageSlot[] = [];\n const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);\n let node = walker.nextNode();\n while (node) {\n const text = node as Text;\n const parent = text.parentElement;\n const value = text.data.trim();\n if (parent && !['STYLE', 'SCRIPT'].includes(parent.tagName) && isMeaningful(value)) {\n const leading = text.data.match(/^\\s*/)?.[0] ?? '';\n const trailing = text.data.match(/\\s*$/)?.[0] ?? '';\n slots.push({ value, apply: (translated) => { text.data = `${leading}${translated}${trailing}`; } });\n }\n node = walker.nextNode();\n }\n for (const element of document.body.querySelectorAll('*')) {\n for (const attribute of TRANSLATABLE_ATTRIBUTES) {\n const value = element.getAttribute(attribute)?.trim();\n if (value && isMeaningful(value)) {\n slots.push({ value, apply: (translated) => element.setAttribute(attribute, translated) });\n }\n }\n }\n return slots;\n};\n\nexport const localizeHtml = async (\n sourceHtml: string,\n sourceLocale: string,\n targetLocale: string,\n translate: (texts: string[]) => Promise<string[]>,\n): Promise<string> => {\n if (targetLocale === sourceLocale) return sourceHtml;\n const document = new DOMParser().parseFromString(sourceHtml, 'text/html');\n document.documentElement.lang = targetLocale;\n const slots = collectMessages(document);\n const translated = await translate(slots.map((slot) => slot.value));\n slots.forEach((slot, index) => slot.apply(translated[index] ?? slot.value));\n return `<!doctype html>${document.documentElement.outerHTML}`;\n};\n\nexport const fingerprint = async (value: string): Promise<string> => {\n const bytes = new TextEncoder().encode(value);\n const hash = await crypto.subtle.digest('SHA-256', bytes);\n return Array.from(new Uint8Array(hash).slice(0, 6), (byte) => byte.toString(16).padStart(2, '0')).join('');\n};\n\nexport const extractPreheader = (html: string): string | undefined => {\n const document = new DOMParser().parseFromString(html, 'text/html');\n const hidden = Array.from(document.body.querySelectorAll<HTMLElement>('[style]')).find((element) =>\n element.style.display === 'none' && element.textContent?.trim(),\n );\n const text = hidden?.textContent?.replace(/[\\u00A0\\u200B-\\u200F\\u202A-\\u202E\\u2066-\\u2069\\uFEFF]/g, '').trim();\n return text || undefined;\n};\n","import { extractPreheader } from '../../core/html';\nimport type { Locale } from '../../core/types';\nimport type { PreviewState } from '../types';\n\ntype PreviewCardProps = {\n locale: Locale;\n preview: PreviewState;\n sourceRevision: string;\n onRefresh: () => void;\n};\n\nexport const PreviewCard = ({ locale, preview, sourceRevision, onRefresh }: PreviewCardProps) => (\n <article className=\"preview-card\">\n <header className=\"preview-header\">\n <div><strong>{locale.label}</strong><span>{locale.code}</span></div>\n <div className={`status status-${preview.status}`}><i />{preview.status}</div>\n </header>\n <div className=\"revision\">source {sourceRevision || '…'} · preview {preview.revision ?? 'pending'}</div>\n <div className=\"preheader\"><span>Preheader</span><strong>{extractPreheader(preview.html) ?? 'Not set'}</strong></div>\n <iframe title={`${locale.label} preview`} srcDoc={preview.html} sandbox=\"allow-popups allow-popups-to-escape-sandbox\" />\n {preview.error && <div className=\"preview-error\">{preview.error}</div>}\n {preview.status !== 'source' && <button className=\"refresh\" onClick={onRefresh}>Regenerate translation</button>}\n </article>\n);\n","const DEFAULT_ROUTE_BASE_PATH = '/preview';\n\nexport const normalizeRouteBasePath = (value = DEFAULT_ROUTE_BASE_PATH) => {\n const normalized = `/${value}`.replace(/\\/+/g, '/').replace(/\\/$/, '');\n return normalized || DEFAULT_ROUTE_BASE_PATH;\n};\n\nexport const templateIdFromUrl = (url: URL, templateIds: string[], routeBasePath?: string) => {\n const basePath = normalizeRouteBasePath(routeBasePath);\n const routeValue = url.pathname.startsWith(`${basePath}/`)\n ? decodeURIComponent(url.pathname.slice(basePath.length + 1))\n : undefined;\n const candidate = routeValue || url.searchParams.get('template') || undefined;\n return candidate && templateIds.includes(candidate) ? candidate : templateIds[0];\n};\n\nexport const localeCodesFromUrl = (url: URL, limit = 3) =>\n url.searchParams.get('langs')?.split(',').filter(Boolean).slice(0, limit) ?? [];\n\nexport const urlForTemplate = (url: URL, templateId: string, routeBasePath?: string) => {\n const next = new URL(url);\n next.pathname = `${normalizeRouteBasePath(routeBasePath)}/${encodeURIComponent(templateId)}`;\n next.searchParams.delete('template');\n return next;\n};\n\nexport const urlForLocales = (url: URL, localeCodes: string[]) => {\n const next = new URL(url);\n if (localeCodes.length) next.searchParams.set('langs', localeCodes.join(','));\n else next.searchParams.delete('langs');\n return next;\n};\n","import { useEffect, useMemo, useState } from 'react';\nimport { renderToStaticMarkup } from 'react-dom/server';\nimport { fingerprint, localizeHtml } from '../../core/html';\nimport type { EmailLabConfig } from '../../core/types';\nimport type { PreviewState } from '../types';\nimport { localeCodesFromUrl, templateIdFromUrl, urlForLocales, urlForTemplate } from '../utils/url-state';\n\nconst LOCALE_LIMIT = 3;\n\nexport const useEmailLab = (config: EmailLabConfig) => {\n const templateIds = useMemo(() => Object.keys(config.templates), [config.templates]);\n const [selectedTemplateId, setSelectedTemplateId] = useState(() =>\n templateIdFromUrl(new URL(window.location.href), templateIds, config.routeBasePath));\n const [activeLocaleCodes, setActiveLocaleCodes] = useState(() =>\n localeCodesFromUrl(new URL(window.location.href), LOCALE_LIMIT));\n const [previews, setPreviews] = useState<Record<string, PreviewState>>({});\n const [sourceRevision, setSourceRevision] = useState('');\n const [generation, setGeneration] = useState(0);\n\n const template = config.templates[selectedTemplateId] ?? config.templates[templateIds[0]];\n const sourceHtml = useMemo(() => `<!doctype html>${renderToStaticMarkup(template.render())}`, [template]);\n const activeLocales = config.locales.filter((locale) => activeLocaleCodes.includes(locale.code));\n const previewLocales = [config.sourceLocale, ...activeLocales];\n\n useEffect(() => {\n const currentId = templateIdFromUrl(new URL(window.location.href), templateIds, config.routeBasePath);\n window.history.replaceState({}, '', urlForTemplate(new URL(window.location.href), currentId, config.routeBasePath));\n\n const restoreUrlState = () => {\n setSelectedTemplateId(templateIdFromUrl(new URL(window.location.href), templateIds, config.routeBasePath));\n setActiveLocaleCodes(localeCodesFromUrl(new URL(window.location.href), LOCALE_LIMIT));\n };\n window.addEventListener('popstate', restoreUrlState);\n return () => window.removeEventListener('popstate', restoreUrlState);\n }, [config.routeBasePath, templateIds]);\n\n useEffect(() => {\n const beforeUpdate = (payload: { updates: Array<{ path: string }> }) => {\n if (payload.updates.some((update) => update.path.includes('/src/emails/'))) window.location.reload();\n };\n import.meta.hot?.on('vite:beforeUpdate', beforeUpdate);\n return () => import.meta.hot?.off('vite:beforeUpdate', beforeUpdate);\n }, []);\n\n useEffect(() => {\n let cancelled = false;\n void fingerprint(sourceHtml).then((revision) => { if (!cancelled) setSourceRevision(revision); });\n return () => { cancelled = true; };\n }, [sourceHtml]);\n\n useEffect(() => {\n let cancelled = false;\n setPreviews((current) => Object.fromEntries(previewLocales.map((locale) => [\n locale.code,\n locale.code === config.sourceLocale.code\n ? { html: sourceHtml, status: 'source' }\n : { html: current[locale.code]?.html ?? sourceHtml, status: 'stale', revision: current[locale.code]?.revision },\n ])));\n\n const translatePreviews = async () => {\n for (const locale of activeLocales) {\n if (cancelled) return;\n setPreviews((current) => ({\n ...current,\n [locale.code]: { ...current[locale.code], status: 'translating', error: undefined },\n }));\n try {\n const html = await localizeHtml(sourceHtml, config.sourceLocale.code, locale.code, (texts) =>\n config.provider.translate({\n texts,\n sourceLocale: config.sourceLocale.translationCode ?? config.sourceLocale.code,\n targetLocale: locale.translationCode ?? locale.code,\n }));\n const revision = await fingerprint(sourceHtml);\n if (!cancelled) {\n setPreviews((current) => ({ ...current, [locale.code]: { html, status: 'ready', revision } }));\n }\n } catch (error) {\n if (!cancelled) {\n setPreviews((current) => ({\n ...current,\n [locale.code]: {\n ...current[locale.code],\n status: 'error',\n error: error instanceof Error ? error.message : String(error),\n },\n }));\n }\n }\n }\n };\n void translatePreviews();\n return () => { cancelled = true; };\n }, [sourceHtml, generation, activeLocaleCodes.join(',')]);\n\n const selectTemplate = (templateId: string) => {\n setSelectedTemplateId(templateId);\n window.history.pushState({}, '', urlForTemplate(new URL(window.location.href), templateId, config.routeBasePath));\n };\n\n const toggleLocale = (localeCode: string) => {\n const next = activeLocaleCodes.includes(localeCode)\n ? activeLocaleCodes.filter((code) => code !== localeCode)\n : [...activeLocaleCodes, localeCode].slice(0, LOCALE_LIMIT);\n setActiveLocaleCodes(next);\n window.history.replaceState({}, '', urlForLocales(new URL(window.location.href), next));\n };\n\n return {\n activeLocaleCodes,\n activeLocales,\n previewLocales,\n previews,\n refreshPreviews: () => setGeneration((value) => value + 1),\n selectTemplate,\n selectedTemplateId,\n sourceHtml,\n sourceRevision,\n templateIds,\n toggleLocale,\n };\n};\n","import { PreviewCard } from './app/components/PreviewCard';\nimport { useEmailLab } from './app/hooks/useEmailLab';\nimport type { EmailLabConfig } from './core/types';\n\nexport const EmailLabApp = ({ config }: { config: EmailLabConfig }) => {\n const lab = useEmailLab(config);\n\n return (\n <main>\n <header className=\"topbar\">\n <div><p className=\"eyebrow\">React Email Locale Lab</p><h1>See every language while you build.</h1></div>\n <div className=\"controls\">\n <label>\n Template\n <select value={lab.selectedTemplateId} onChange={(event) => lab.selectTemplate(event.target.value)}>\n {lab.templateIds.map((id) => <option key={id} value={id}>{config.templates[id].name}</option>)}\n </select>\n </label>\n <span className=\"provider\">Provider: {config.provider.name}</span>\n </div>\n </header>\n\n <section className=\"locale-picker\">\n <div><strong>Preview languages</strong><span>Select up to three. Language packs load only when selected.</span></div>\n <div className=\"locale-options\">\n {config.locales.map((locale) => (\n <button\n className={lab.activeLocaleCodes.includes(locale.code) ? 'locale-active' : ''}\n disabled={!lab.activeLocaleCodes.includes(locale.code) && lab.activeLocaleCodes.length >= 3}\n key={locale.code}\n onClick={() => lab.toggleLocale(locale.code)}\n >\n <i />{locale.label}\n </button>\n ))}\n </div>\n </section>\n\n <section className=\"notice\"><span className=\"live-dot\" />Watching source changes through Vite HMR.</section>\n {lab.activeLocales.length === 0 && (\n <section className=\"empty-state\">\n <strong>Select a language to generate a preview.</strong>\n <span>The source template renders immediately; translation starts only on demand.</span>\n </section>\n )}\n <section className=\"grid\">\n {lab.previewLocales.map((locale) => (\n <PreviewCard\n key={locale.code}\n locale={locale}\n preview={lab.previews[locale.code] ?? { html: lab.sourceHtml, status: 'stale' }}\n sourceRevision={lab.sourceRevision}\n onRefresh={lab.refreshPreviews}\n />\n ))}\n </section>\n </main>\n );\n};\n","import type { TranslationProvider } from './types';\n\ntype BrowserTranslator = { translate: (text: string) => Promise<string>; destroy?: () => void };\ntype TranslatorFactory = {\n availability: (options: { sourceLanguage: string; targetLanguage: string }) => Promise<string>;\n create: (options: { sourceLanguage: string; targetLanguage: string }) => Promise<BrowserTranslator>;\n};\n\ndeclare global {\n interface Window { Translator?: TranslatorFactory }\n}\n\nexport const browserTranslatorProvider = (): TranslationProvider => {\n const translators = new Map<string, Promise<BrowserTranslator>>();\n const cache = new Map<string, string>();\n const queues = new Map<string, Promise<void>>();\n\n const serialize = async <T,>(pair: string, task: () => Promise<T>): Promise<T> => {\n const previous = queues.get(pair) ?? Promise.resolve();\n let release = () => {};\n const turn = new Promise<void>((resolve) => { release = resolve; });\n const queued = previous.catch(() => undefined).then(() => turn);\n queues.set(pair, queued);\n await previous.catch(() => undefined);\n try {\n return await task();\n } finally {\n release();\n if (queues.get(pair) === queued) queues.delete(pair);\n }\n };\n\n const getTranslator = async (sourceLocale: string, targetLocale: string) => {\n if (!window.Translator) {\n throw new Error('This browser does not support the built-in Translator API. Use a current Chrome build or configure a remote provider.');\n }\n const pair = `${sourceLocale}:${targetLocale}`;\n let translator = translators.get(pair);\n if (!translator) {\n const availability = await window.Translator.availability({ sourceLanguage: sourceLocale, targetLanguage: targetLocale });\n if (availability === 'unavailable') throw new Error(`The browser cannot translate ${sourceLocale} → ${targetLocale}.`);\n translator = window.Translator.create({ sourceLanguage: sourceLocale, targetLanguage: targetLocale });\n translators.set(pair, translator);\n }\n return translator;\n };\n\n return {\n name: 'Browser Translator · on-device',\n async translate({ texts, sourceLocale, targetLocale }) {\n if (texts.length === 0) return [];\n const translator = await getTranslator(sourceLocale, targetLocale);\n const pair = `${sourceLocale}:${targetLocale}`;\n return serialize(pair, async () => {\n const results: string[] = [];\n for (const text of texts) {\n const key = `${pair}:${text}`;\n let translated = cache.get(key);\n if (!translated) {\n translated = await translator.translate(text);\n cache.set(key, translated);\n }\n results.push(translated);\n }\n return results;\n });\n },\n };\n};\n","import type { ReactElement } from 'react';\n\nexport type Locale = {\n code: string;\n label: string;\n translationCode?: string;\n};\nexport type EmailTemplate = { name: string; render: () => ReactElement };\nexport type TranslationRequest = { texts: string[]; sourceLocale: string; targetLocale: string };\nexport type TranslationProvider = {\n name: string;\n translate: (request: TranslationRequest) => Promise<string[]>;\n};\nexport type EmailLabConfig = {\n sourceLocale: Locale;\n locales: Locale[];\n templates: Record<string, EmailTemplate>;\n provider: TranslationProvider;\n routeBasePath?: string;\n};\n\nexport const defineEmailLab = (config: EmailLabConfig): EmailLabConfig => config;\n"],"names":["TRANSLATABLE_ATTRIBUTES","isMeaningful","value","collectMessages","document","slots","walker","node","text","parent","leading","trailing","translated","element","attribute","localizeHtml","sourceHtml","sourceLocale","targetLocale","translate","slot","index","fingerprint","bytes","hash","byte","extractPreheader","html","PreviewCard","locale","preview","sourceRevision","onRefresh","jsxs","jsx","DEFAULT_ROUTE_BASE_PATH","normalizeRouteBasePath","templateIdFromUrl","url","templateIds","routeBasePath","basePath","candidate","localeCodesFromUrl","limit","urlForTemplate","templateId","next","urlForLocales","localeCodes","LOCALE_LIMIT","useEmailLab","config","useMemo","selectedTemplateId","setSelectedTemplateId","useState","activeLocaleCodes","setActiveLocaleCodes","previews","setPreviews","setSourceRevision","generation","setGeneration","template","renderToStaticMarkup","activeLocales","previewLocales","useEffect","currentId","restoreUrlState","cancelled","revision","current","texts","error","localeCode","code","EmailLabApp","lab","event","id","browserTranslatorProvider","translators","cache","queues","serialize","pair","task","previous","release","turn","resolve","queued","getTranslator","translator","results","key","defineEmailLab"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ :root{color:#182033;background:#f4f5f9;font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-synthesis:none}*{box-sizing:border-box}body{margin:0;min-width:320px}button,select{font:inherit}main{margin:0 auto;max-width:1640px;padding:42px clamp(18px,4vw,64px) 70px}.topbar{align-items:end;display:flex;justify-content:space-between;gap:32px}.eyebrow{color:#615ee8;font-size:12px;font-weight:800;letter-spacing:.12em;margin:0 0 8px;text-transform:uppercase}h1{font-size:clamp(30px,4vw,50px);letter-spacing:-.045em;line-height:1;margin:0}.controls{align-items:end;display:flex;gap:12px}label{color:#72788a;display:grid;font-size:12px;font-weight:700;gap:6px}select{background:#fff;border:1px solid #dfe1e8;border-radius:10px;color:#182033;min-width:210px;padding:11px 36px 11px 12px}.provider{background:#e8e7ff;border-radius:999px;color:#514dcf;font-size:12px;font-weight:700;padding:10px 13px}.notice{align-items:center;background:#fff;border:1px solid #e2e4ea;border-radius:12px;display:flex;font-size:13px;margin:30px 0 20px;padding:13px 16px}.locale-picker{align-items:center;background:#fff;border:1px solid #e2e4ea;border-radius:14px;display:flex;justify-content:space-between;margin-top:30px;padding:16px}.locale-picker strong,.locale-picker span{display:block}.locale-picker strong{font-size:13px}.locale-picker span{color:#8c91a0;font-size:11px;margin-top:3px}.locale-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end}.locale-options button{align-items:center;background:#f6f7fa;border:1px solid #e2e4ea;border-radius:999px;color:#686e7e;cursor:pointer;display:flex;font-size:12px;font-weight:700;gap:7px;padding:8px 11px}.locale-options button i{border:1px solid #b7bbc7;border-radius:50%;height:8px;width:8px}.locale-options button.locale-active{background:#e8e7ff;border-color:#c7c5ff;color:#514dcf}.locale-options button.locale-active i{background:#615ee8;border-color:#615ee8}.locale-options button:disabled{cursor:not-allowed;opacity:.4}.notice code{background:#f0f1f5;border-radius:5px;margin-left:4px;padding:2px 5px}.live-dot{animation:pulse 1.7s infinite;background:#16a56d;border-radius:50%;height:8px;margin-right:10px;width:8px}.grid{display:grid;gap:18px;grid-template-columns:repeat(2,minmax(0,1fr))}.preview-card{background:#fff;border:1px solid #dfe1e8;border-radius:16px;box-shadow:0 8px 28px #1b233a0d;overflow:hidden;position:relative}.preview-header{align-items:center;border-bottom:1px solid #eceef3;display:flex;justify-content:space-between;padding:15px 17px 12px}.preview-header strong{display:block;font-size:14px}.preview-header span{color:#979cab;font-size:11px}.status{align-items:center;border-radius:999px;display:flex;font-size:11px;font-weight:800;gap:6px;padding:5px 9px;text-transform:uppercase}.status i{background:currentColor;border-radius:50%;height:6px;width:6px}.status-source,.status-ready{background:#e6f7ef;color:#168659}.status-stale{background:#fff2d7;color:#b17100}.status-translating{background:#e8e7ff;color:#5753d6}.status-translating i{animation:pulse .8s infinite}.status-error{background:#ffe8e8;color:#c53c3c}.revision{background:#fafbfc;color:#9ba0ad;font-family:ui-monospace,SFMono-Regular,monospace;font-size:10px;padding:7px 17px}.preheader{align-items:baseline;border-bottom:1px solid #eceef3;display:flex;gap:9px;padding:10px 17px}.preheader span{color:#979cab;font-size:10px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.preheader strong{color:#4f5565;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}iframe{background:#edf0f7;border:0;display:block;height:590px;width:100%}.refresh{background:#fff;border:1px solid #dfe1e8;border-radius:8px;bottom:12px;color:#595f70;cursor:pointer;font-size:11px;font-weight:700;padding:7px 10px;position:absolute;right:12px}.refresh:hover{border-color:#615ee8;color:#514dcf}.preview-error{background:#fff2f2;border-top:1px solid #ffd5d5;bottom:0;color:#a63333;font-size:12px;left:0;padding:11px 130px 11px 14px;position:absolute;right:0}.empty-state{align-items:center;background:#fff;border:1px dashed #cfd2dc;border-radius:16px;display:flex;flex-direction:column;justify-content:center;min-height:220px;text-align:center}.empty-state span{color:#8b90a0;font-size:13px;margin-top:7px}@keyframes pulse{50%{opacity:.3;transform:scale(.82)}}@media(max-width:900px){.grid{grid-template-columns:1fr}.topbar,.locale-picker{align-items:start;flex-direction:column}.controls{align-items:stretch;flex-direction:column;width:100%}.locale-options{justify-content:flex-start}}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "react-email-locale-lab",
3
+ "version": "0.1.0",
4
+ "description": "Real-time multi-language previews for React Email templates.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ },
18
+ "./app": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./browser-translator": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ },
26
+ "./styles.css": "./dist/styles.css"
27
+ },
28
+ "sideEffects": ["**/*.css"],
29
+ "keywords": ["react-email", "email", "i18n", "localization", "preview"],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "dev": "vite",
35
+ "build": "pnpm build:lib",
36
+ "build:lib": "vite build --config vite.lib.config.ts && tsc -p tsconfig.lib.json",
37
+ "build:demo": "tsc -b && vite build",
38
+ "test": "vitest run --maxWorkers=1",
39
+ "check": "pnpm test && pnpm build",
40
+ "pack:dry-run": "pnpm build && npm pack --dry-run",
41
+ "prepublishOnly": "pnpm check"
42
+ },
43
+ "peerDependencies": {
44
+ "react": "^18.0.0 || ^19.0.0",
45
+ "react-dom": "^18.0.0 || ^19.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/react": "^19.0.0",
49
+ "@types/react-dom": "^19.0.0",
50
+ "@vitejs/plugin-react": "^4.0.0",
51
+ "react": "^19.0.0",
52
+ "react-dom": "^19.0.0",
53
+ "react-email": "^6.9.0",
54
+ "typescript": "^5.9.0",
55
+ "vite": "^7.0.0",
56
+ "vitest": "^3.0.0"
57
+ },
58
+ "packageManager": "pnpm@10.11.0"
59
+ }