react-email-locale-lab 0.1.0 → 0.2.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 CHANGED
@@ -4,7 +4,7 @@ React Email Locale Lab is a proof of concept for previewing React Email template
4
4
 
5
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
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.
7
+ This project exists to test the technical and product viability of that workflow.
8
8
 
9
9
  ## Install
10
10
 
@@ -30,7 +30,7 @@ const config = defineEmailLab({
30
30
  locales: [{ code: 'de', label: 'Deutsch' }],
31
31
  provider: browserTranslatorProvider(),
32
32
  templates: {
33
- welcome: { name: 'Welcome', render: () => <WelcomeEmail /> },
33
+ welcome: { name: 'Welcome', component: WelcomeEmail },
34
34
  },
35
35
  });
36
36
 
@@ -62,11 +62,23 @@ export default defineEmailLab({
62
62
  ],
63
63
  provider: browserTranslatorProvider(),
64
64
  templates: {
65
- welcome: { name: 'Welcome', render: () => <WelcomeEmail /> },
65
+ welcome: { name: 'Welcome', component: WelcomeEmail },
66
66
  },
67
67
  });
68
68
  ```
69
69
 
70
+ When a template declares React Email `PreviewProps`, the lab uses them automatically:
71
+
72
+ ```tsx
73
+ WelcomeEmail.PreviewProps = { customerName: 'Taylor' };
74
+
75
+ templates: {
76
+ welcome: { name: 'Welcome', component: WelcomeEmail },
77
+ }
78
+ ```
79
+
80
+ Set `props` on the template entry to override `PreviewProps`. A custom `render` function remains available for templates that need bespoke setup.
81
+
70
82
  ## Template routes
71
83
 
72
84
  Every configured template has a stable preview route:
package/dist/App.d.ts CHANGED
@@ -2,4 +2,3 @@ import type { EmailLabConfig } from './core/types';
2
2
  export declare const EmailLabApp: ({ config }: {
3
3
  config: EmailLabConfig;
4
4
  }) => import("react").JSX.Element;
5
- //# sourceMappingURL=App.d.ts.map
@@ -8,4 +8,3 @@ type PreviewCardProps = {
8
8
  };
9
9
  export declare const PreviewCard: ({ locale, preview, sourceRevision, onRefresh }: PreviewCardProps) => import("react").JSX.Element;
10
10
  export {};
11
- //# sourceMappingURL=PreviewCard.d.ts.map
@@ -13,4 +13,3 @@ export declare const useEmailLab: (config: EmailLabConfig) => {
13
13
  templateIds: string[];
14
14
  toggleLocale: (localeCode: string) => void;
15
15
  };
16
- //# sourceMappingURL=useEmailLab.d.ts.map
@@ -5,4 +5,3 @@ export type PreviewState = {
5
5
  revision?: string;
6
6
  error?: string;
7
7
  };
8
- //# sourceMappingURL=types.d.ts.map
@@ -3,4 +3,3 @@ export declare const templateIdFromUrl: (url: URL, templateIds: string[], routeB
3
3
  export declare const localeCodesFromUrl: (url: URL, limit?: number) => string[];
4
4
  export declare const urlForTemplate: (url: URL, templateId: string, routeBasePath?: string) => URL;
5
5
  export declare const urlForLocales: (url: URL, localeCodes: string[]) => URL;
6
- //# sourceMappingURL=url-state.d.ts.map
@@ -20,4 +20,3 @@ declare global {
20
20
  }
21
21
  export declare const browserTranslatorProvider: () => TranslationProvider;
22
22
  export {};
23
- //# sourceMappingURL=browser-translator-provider.d.ts.map
@@ -6,4 +6,3 @@ export declare const collectMessages: (document: Document) => MessageSlot[];
6
6
  export declare const localizeHtml: (sourceHtml: string, sourceLocale: string, targetLocale: string, translate: (texts: string[]) => Promise<string[]>) => Promise<string>;
7
7
  export declare const fingerprint: (value: string) => Promise<string>;
8
8
  export declare const extractPreheader: (html: string) => string | undefined;
9
- //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1,3 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { EmailTemplate } from './types';
3
+ export declare const renderEmailTemplate: (template: EmailTemplate) => ReactElement;
@@ -1,13 +1,23 @@
1
- import type { ReactElement } from 'react';
1
+ import type { ComponentType, ReactElement } from 'react';
2
2
  export type Locale = {
3
3
  code: string;
4
4
  label: string;
5
5
  translationCode?: string;
6
6
  };
7
+ export type PreviewableEmailComponent = ComponentType<any> & {
8
+ PreviewProps?: Record<string, unknown>;
9
+ };
7
10
  export type EmailTemplate = {
8
11
  name: string;
12
+ } & ({
13
+ component: PreviewableEmailComponent;
14
+ props?: Record<string, unknown>;
15
+ render?: never;
16
+ } | {
9
17
  render: () => ReactElement;
10
- };
18
+ component?: never;
19
+ props?: never;
20
+ });
11
21
  export type TranslationRequest = {
12
22
  texts: string[];
13
23
  sourceLocale: string;
@@ -25,4 +35,3 @@ export type EmailLabConfig = {
25
35
  routeBasePath?: string;
26
36
  };
27
37
  export declare const defineEmailLab: (config: EmailLabConfig) => EmailLabConfig;
28
- //# sourceMappingURL=types.d.ts.map
package/dist/index.d.ts CHANGED
@@ -3,4 +3,3 @@ export { EmailLabApp } from './App';
3
3
  export { browserTranslatorProvider } from './core/browser-translator-provider';
4
4
  export { defineEmailLab } from './core/types';
5
5
  export type { EmailLabConfig, EmailTemplate, Locale, TranslationProvider, TranslationRequest, } from './core/types';
6
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,79 +1,83 @@
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();
1
+ import { jsxs as d, jsx as c } from "react/jsx-runtime";
2
+ import { createElement as I, useMemo as A, useState as b, useEffect as T } from "react";
3
+ import { renderToStaticMarkup as M } from "react-dom/server";
4
+ const k = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").test(e), j = (e) => {
5
+ const t = [], r = e.createTreeWalker(e.body, NodeFilter.SHOW_TEXT);
6
+ let n = r.nextNode();
7
7
  for (; n; ) {
8
- const s = n, r = s.parentElement, c = s.data.trim();
9
- if (r && !["STYLE", "SCRIPT"].includes(r.tagName) && $(c)) {
8
+ const s = n, a = s.parentElement, l = s.data.trim();
9
+ if (a && !["STYLE", "SCRIPT"].includes(a.tagName) && $(l)) {
10
10
  const o = s.data.match(/^\s*/)?.[0] ?? "", u = s.data.match(/\s*$/)?.[0] ?? "";
11
- t.push({ value: c, apply: (w) => {
11
+ t.push({ value: l, apply: (w) => {
12
12
  s.data = `${o}${w}${u}`;
13
13
  } });
14
14
  }
15
- n = a.nextNode();
15
+ n = r.nextNode();
16
16
  }
17
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) });
18
+ for (const a of k) {
19
+ const l = s.getAttribute(a)?.trim();
20
+ l && $(l) && t.push({ value: l, apply: (o) => s.setAttribute(a, o) });
21
21
  }
22
22
  return t;
23
- }, j = async (e, t, a, n) => {
24
- if (a === t) return e;
23
+ }, H = async (e, t, r, n) => {
24
+ if (r === t) return e;
25
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}`;
26
+ s.documentElement.lang = r;
27
+ const a = j(s), l = await n(a.map((o) => o.value));
28
+ return a.forEach((o, u) => o.apply(l[u] ?? o.value)), `<!doctype html>${s.documentElement.outerHTML}`;
29
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) => {
30
+ const t = new TextEncoder().encode(e), r = await crypto.subtle.digest("SHA-256", t);
31
+ return Array.from(new Uint8Array(r).slice(0, 6), (n) => n.toString(16).padStart(2, "0")).join("");
32
+ }, O = (e) => {
33
33
  const t = new DOMParser().parseFromString(e, "text/html");
34
34
  return Array.from(t.body.querySelectorAll("[style]")).find(
35
35
  (s) => s.style.display === "none" && s.textContent?.trim()
36
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: [
37
+ }, _ = ({ locale: e, preview: t, sourceRevision: r, onRefresh: n }) => /* @__PURE__ */ d("article", { className: "preview-card", children: [
38
38
  /* @__PURE__ */ d("header", { className: "preview-header", children: [
39
39
  /* @__PURE__ */ d("div", { children: [
40
- /* @__PURE__ */ l("strong", { children: e.label }),
41
- /* @__PURE__ */ l("span", { children: e.code })
40
+ /* @__PURE__ */ c("strong", { children: e.label }),
41
+ /* @__PURE__ */ c("span", { children: e.code })
42
42
  ] }),
43
43
  /* @__PURE__ */ d("div", { className: `status status-${t.status}`, children: [
44
- /* @__PURE__ */ l("i", {}),
44
+ /* @__PURE__ */ c("i", {}),
45
45
  t.status
46
46
  ] })
47
47
  ] }),
48
48
  /* @__PURE__ */ d("div", { className: "revision", children: [
49
49
  "source ",
50
- a || "…",
50
+ r || "…",
51
51
  " · preview ",
52
52
  t.revision ?? "pending"
53
53
  ] }),
54
54
  /* @__PURE__ */ d("div", { className: "preheader", children: [
55
- /* @__PURE__ */ l("span", { children: "Preheader" }),
56
- /* @__PURE__ */ l("strong", { children: H(t.html) ?? "Not set" })
55
+ /* @__PURE__ */ c("span", { children: "Preheader" }),
56
+ /* @__PURE__ */ c("strong", { children: O(t.html) ?? "Not set" })
57
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) => {
58
+ /* @__PURE__ */ c("iframe", { title: `${e.label} preview`, srcDoc: t.html, sandbox: "allow-popups allow-popups-to-escape-sandbox" }),
59
+ t.error && /* @__PURE__ */ c("div", { className: "preview-error", children: t.error }),
60
+ t.status !== "source" && /* @__PURE__ */ c("button", { className: "refresh", onClick: n, children: "Regenerate translation" })
61
+ ] }), z = (e) => {
62
+ if ("render" in e && e.render) return e.render();
63
+ const t = e.props ?? e.component.PreviewProps ?? {};
64
+ return I(e.component, t);
65
+ }, x = "/preview", F = (e = x) => `/${e}`.replace(/\/+/g, "/").replace(/\/$/, "") || x, R = (e, t, r) => {
66
+ const n = F(r), a = (e.pathname.startsWith(`${n}/`) ? decodeURIComponent(e.pathname.slice(n.length + 1)) : void 0) || e.searchParams.get("template") || void 0;
67
+ return a && t.includes(a) ? a : t[0];
68
+ }, C = (e, t = 3) => e.searchParams.get("langs")?.split(",").filter(Boolean).slice(0, t) ?? [], B = (e, t, r) => {
65
69
  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];
70
+ return n.pathname = `${F(r)}/${encodeURIComponent(t)}`, n.searchParams.delete("template"), n;
71
+ }, q = (e, t) => {
72
+ const r = new URL(e);
73
+ return t.length ? r.searchParams.set("langs", t.join(",")) : r.searchParams.delete("langs"), r;
74
+ }, S = 3, D = (e) => {
75
+ const t = A(() => Object.keys(e.templates), [e.templates]), [r, n] = b(() => R(new URL(window.location.href), t, e.routeBasePath)), [s, a] = b(() => C(new URL(window.location.href), S)), [l, o] = b({}), [u, w] = b(""), [g, y] = b(0), L = e.templates[r] ?? e.templates[t[0]], h = A(() => `<!doctype html>${M(z(L))}`, [L]), P = e.locales.filter((i) => s.includes(i.code)), N = [e.sourceLocale, ...P];
72
76
  return T(() => {
73
77
  const i = R(new URL(window.location.href), t, e.routeBasePath);
74
78
  window.history.replaceState({}, "", B(new URL(window.location.href), i, e.routeBasePath));
75
79
  const v = () => {
76
- n(R(new URL(window.location.href), t, e.routeBasePath)), r(C(new URL(window.location.href), S));
80
+ n(R(new URL(window.location.href), t, e.routeBasePath)), a(C(new URL(window.location.href), S));
77
81
  };
78
82
  return window.addEventListener("popstate", v), () => window.removeEventListener("popstate", v);
79
83
  }, [e.routeBasePath, t]), T(() => () => {
@@ -97,7 +101,7 @@ const M = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
97
101
  [m.code]: { ...p[m.code], status: "translating", error: void 0 }
98
102
  }));
99
103
  try {
100
- const p = await j(h, e.sourceLocale.code, m.code, (E) => e.provider.translate({
104
+ const p = await H(h, e.sourceLocale.code, m.code, (E) => e.provider.translate({
101
105
  texts: E,
102
106
  sourceLocale: e.sourceLocale.translationCode ?? e.sourceLocale.code,
103
107
  targetLocale: m.translationCode ?? m.code
@@ -121,32 +125,32 @@ const M = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
121
125
  activeLocaleCodes: s,
122
126
  activeLocales: P,
123
127
  previewLocales: N,
124
- previews: c,
128
+ previews: l,
125
129
  refreshPreviews: () => y((i) => i + 1),
126
130
  selectTemplate: (i) => {
127
131
  n(i), window.history.pushState({}, "", B(new URL(window.location.href), i, e.routeBasePath));
128
132
  },
129
- selectedTemplateId: a,
133
+ selectedTemplateId: r,
130
134
  sourceHtml: h,
131
135
  sourceRevision: u,
132
136
  templateIds: t,
133
137
  toggleLocale: (i) => {
134
138
  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));
139
+ a(v), window.history.replaceState({}, "", q(new URL(window.location.href), v));
136
140
  }
137
141
  };
138
- }, X = ({ config: e }) => {
139
- const t = z(e);
142
+ }, J = ({ config: e }) => {
143
+ const t = D(e);
140
144
  return /* @__PURE__ */ d("main", { children: [
141
145
  /* @__PURE__ */ d("header", { className: "topbar", children: [
142
146
  /* @__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." })
147
+ /* @__PURE__ */ c("p", { className: "eyebrow", children: "React Email Locale Lab" }),
148
+ /* @__PURE__ */ c("h1", { children: "See every language while you build." })
145
149
  ] }),
146
150
  /* @__PURE__ */ d("div", { className: "controls", children: [
147
151
  /* @__PURE__ */ d("label", { children: [
148
152
  "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)) })
153
+ /* @__PURE__ */ c("select", { value: t.selectedTemplateId, onChange: (r) => t.selectTemplate(r.target.value), children: t.templateIds.map((r) => /* @__PURE__ */ c("option", { value: r, children: e.templates[r].name }, r)) })
150
154
  ] }),
151
155
  /* @__PURE__ */ d("span", { className: "provider", children: [
152
156
  "Provider: ",
@@ -156,77 +160,77 @@ const M = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
156
160
  ] }),
157
161
  /* @__PURE__ */ d("section", { className: "locale-picker", children: [
158
162
  /* @__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." })
163
+ /* @__PURE__ */ c("strong", { children: "Preview languages" }),
164
+ /* @__PURE__ */ c("span", { children: "Select up to three. Language packs load only when selected." })
161
165
  ] }),
162
- /* @__PURE__ */ l("div", { className: "locale-options", children: e.locales.map((a) => /* @__PURE__ */ d(
166
+ /* @__PURE__ */ c("div", { className: "locale-options", children: e.locales.map((r) => /* @__PURE__ */ d(
163
167
  "button",
164
168
  {
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),
169
+ className: t.activeLocaleCodes.includes(r.code) ? "locale-active" : "",
170
+ disabled: !t.activeLocaleCodes.includes(r.code) && t.activeLocaleCodes.length >= 3,
171
+ onClick: () => t.toggleLocale(r.code),
168
172
  children: [
169
- /* @__PURE__ */ l("i", {}),
170
- a.label
173
+ /* @__PURE__ */ c("i", {}),
174
+ r.label
171
175
  ]
172
176
  },
173
- a.code
177
+ r.code
174
178
  )) })
175
179
  ] }),
176
180
  /* @__PURE__ */ d("section", { className: "notice", children: [
177
- /* @__PURE__ */ l("span", { className: "live-dot" }),
181
+ /* @__PURE__ */ c("span", { className: "live-dot" }),
178
182
  "Watching source changes through Vite HMR."
179
183
  ] }),
180
184
  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." })
185
+ /* @__PURE__ */ c("strong", { children: "Select a language to generate a preview." }),
186
+ /* @__PURE__ */ c("span", { children: "The source template renders immediately; translation starts only on demand." })
183
187
  ] }),
184
- /* @__PURE__ */ l("section", { className: "grid", children: t.previewLocales.map((a) => /* @__PURE__ */ l(
185
- O,
188
+ /* @__PURE__ */ c("section", { className: "grid", children: t.previewLocales.map((r) => /* @__PURE__ */ c(
189
+ _,
186
190
  {
187
- locale: a,
188
- preview: t.previews[a.code] ?? { html: t.sourceHtml, status: "stale" },
191
+ locale: r,
192
+ preview: t.previews[r.code] ?? { html: t.sourceHtml, status: "stale" },
189
193
  sourceRevision: t.sourceRevision,
190
194
  onRefresh: t.refreshPreviews
191
195
  },
192
- a.code
196
+ r.code
193
197
  )) })
194
198
  ] });
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();
199
+ }, K = () => {
200
+ const e = /* @__PURE__ */ new Map(), t = /* @__PURE__ */ new Map(), r = /* @__PURE__ */ new Map(), n = async (a, l) => {
201
+ const o = r.get(a) ?? Promise.resolve();
198
202
  let u = () => {
199
203
  };
200
204
  const w = new Promise((y) => {
201
205
  u = y;
202
206
  }), g = o.catch(() => {
203
207
  }).then(() => w);
204
- a.set(r, g), await o.catch(() => {
208
+ r.set(a, g), await o.catch(() => {
205
209
  });
206
210
  try {
207
- return await c();
211
+ return await l();
208
212
  } finally {
209
- u(), a.get(r) === g && a.delete(r);
213
+ u(), r.get(a) === g && r.delete(a);
210
214
  }
211
- }, s = async (r, c) => {
215
+ }, s = async (a, l) => {
212
216
  if (!window.Translator)
213
217
  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}`;
218
+ const o = `${a}:${l}`;
215
219
  let u = e.get(o);
216
220
  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);
221
+ if (await window.Translator.availability({ sourceLanguage: a, targetLanguage: l }) === "unavailable") throw new Error(`The browser cannot translate ${a} → ${l}.`);
222
+ u = window.Translator.create({ sourceLanguage: a, targetLanguage: l }), e.set(o, u);
219
223
  }
220
224
  return u;
221
225
  };
222
226
  return {
223
227
  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}`;
228
+ async translate({ texts: a, sourceLocale: l, targetLocale: o }) {
229
+ if (a.length === 0) return [];
230
+ const u = await s(l, o), w = `${l}:${o}`;
227
231
  return n(w, async () => {
228
232
  const g = [];
229
- for (const y of r) {
233
+ for (const y of a) {
230
234
  const L = `${w}:${y}`;
231
235
  let h = t.get(L);
232
236
  h || (h = await u.translate(y), t.set(L, h)), g.push(h);
@@ -235,10 +239,9 @@ const M = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
235
239
  });
236
240
  }
237
241
  };
238
- }, J = (e) => e;
242
+ }, Q = (e) => e;
239
243
  export {
240
- X as EmailLabApp,
241
- Y as browserTranslatorProvider,
242
- J as defineEmailLab
244
+ J as EmailLabApp,
245
+ K as browserTranslatorProvider,
246
+ Q as defineEmailLab
243
247
  };
244
- //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email-locale-lab",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Real-time multi-language previews for React Email templates.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,17 +15,9 @@
15
15
  "types": "./dist/index.d.ts",
16
16
  "import": "./dist/index.js"
17
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
18
  "./styles.css": "./dist/styles.css"
27
19
  },
28
- "sideEffects": ["**/*.css"],
20
+ "sideEffects": ["./dist/styles.css"],
29
21
  "keywords": ["react-email", "email", "i18n", "localization", "preview"],
30
22
  "publishConfig": {
31
23
  "access": "public"
package/dist/App.d.ts.map DELETED
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
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.map DELETED
@@ -1 +0,0 @@
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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"}