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 +15 -3
- package/dist/App.d.ts +0 -1
- package/dist/app/components/PreviewCard.d.ts +0 -1
- package/dist/app/hooks/useEmailLab.d.ts +0 -1
- package/dist/app/types.d.ts +0 -1
- package/dist/app/utils/url-state.d.ts +0 -1
- package/dist/core/browser-translator-provider.d.ts +0 -1
- package/dist/core/html.d.ts +0 -1
- package/dist/core/template.d.ts +3 -0
- package/dist/core/types.d.ts +12 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.js +90 -87
- package/package.json +2 -10
- package/dist/App.d.ts.map +0 -1
- package/dist/app/components/PreviewCard.d.ts.map +0 -1
- package/dist/app/hooks/useEmailLab.d.ts.map +0 -1
- package/dist/app/types.d.ts.map +0 -1
- package/dist/app/utils/url-state.d.ts.map +0 -1
- package/dist/core/browser-translator-provider.d.ts.map +0 -1
- package/dist/core/html.d.ts.map +0 -1
- package/dist/core/types.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
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.
|
|
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',
|
|
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',
|
|
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
package/dist/app/types.d.ts
CHANGED
|
@@ -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
|
package/dist/core/html.d.ts
CHANGED
|
@@ -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
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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
|
|
2
|
-
import { useMemo as A, useState as b, useEffect as T } from "react";
|
|
3
|
-
import { renderToStaticMarkup as
|
|
4
|
-
const
|
|
5
|
-
const t = [],
|
|
6
|
-
let n =
|
|
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,
|
|
9
|
-
if (
|
|
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:
|
|
11
|
+
t.push({ value: l, apply: (w) => {
|
|
12
12
|
s.data = `${o}${w}${u}`;
|
|
13
13
|
} });
|
|
14
14
|
}
|
|
15
|
-
n =
|
|
15
|
+
n = r.nextNode();
|
|
16
16
|
}
|
|
17
17
|
for (const s of e.body.querySelectorAll("*"))
|
|
18
|
-
for (const
|
|
19
|
-
const
|
|
20
|
-
|
|
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
|
-
},
|
|
24
|
-
if (
|
|
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 =
|
|
27
|
-
const
|
|
28
|
-
return
|
|
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),
|
|
31
|
-
return Array.from(new Uint8Array(
|
|
32
|
-
},
|
|
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
|
-
},
|
|
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__ */
|
|
41
|
-
/* @__PURE__ */
|
|
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__ */
|
|
44
|
+
/* @__PURE__ */ c("i", {}),
|
|
45
45
|
t.status
|
|
46
46
|
] })
|
|
47
47
|
] }),
|
|
48
48
|
/* @__PURE__ */ d("div", { className: "revision", children: [
|
|
49
49
|
"source ",
|
|
50
|
-
|
|
50
|
+
r || "…",
|
|
51
51
|
" · preview ",
|
|
52
52
|
t.revision ?? "pending"
|
|
53
53
|
] }),
|
|
54
54
|
/* @__PURE__ */ d("div", { className: "preheader", children: [
|
|
55
|
-
/* @__PURE__ */
|
|
56
|
-
/* @__PURE__ */
|
|
55
|
+
/* @__PURE__ */ c("span", { children: "Preheader" }),
|
|
56
|
+
/* @__PURE__ */ c("strong", { children: O(t.html) ?? "Not set" })
|
|
57
57
|
] }),
|
|
58
|
-
/* @__PURE__ */
|
|
59
|
-
t.error && /* @__PURE__ */
|
|
60
|
-
t.status !== "source" && /* @__PURE__ */
|
|
61
|
-
] }),
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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(
|
|
67
|
-
},
|
|
68
|
-
const
|
|
69
|
-
return t.length ?
|
|
70
|
-
}, S = 3,
|
|
71
|
-
const t = A(() => Object.keys(e.templates), [e.templates]), [
|
|
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)),
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
139
|
+
a(v), window.history.replaceState({}, "", q(new URL(window.location.href), v));
|
|
136
140
|
}
|
|
137
141
|
};
|
|
138
|
-
},
|
|
139
|
-
const t =
|
|
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__ */
|
|
144
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
160
|
-
/* @__PURE__ */
|
|
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__ */
|
|
166
|
+
/* @__PURE__ */ c("div", { className: "locale-options", children: e.locales.map((r) => /* @__PURE__ */ d(
|
|
163
167
|
"button",
|
|
164
168
|
{
|
|
165
|
-
className: t.activeLocaleCodes.includes(
|
|
166
|
-
disabled: !t.activeLocaleCodes.includes(
|
|
167
|
-
onClick: () => t.toggleLocale(
|
|
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__ */
|
|
170
|
-
|
|
173
|
+
/* @__PURE__ */ c("i", {}),
|
|
174
|
+
r.label
|
|
171
175
|
]
|
|
172
176
|
},
|
|
173
|
-
|
|
177
|
+
r.code
|
|
174
178
|
)) })
|
|
175
179
|
] }),
|
|
176
180
|
/* @__PURE__ */ d("section", { className: "notice", children: [
|
|
177
|
-
/* @__PURE__ */
|
|
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__ */
|
|
182
|
-
/* @__PURE__ */
|
|
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__ */
|
|
185
|
-
|
|
188
|
+
/* @__PURE__ */ c("section", { className: "grid", children: t.previewLocales.map((r) => /* @__PURE__ */ c(
|
|
189
|
+
_,
|
|
186
190
|
{
|
|
187
|
-
locale:
|
|
188
|
-
preview: t.previews[
|
|
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
|
-
|
|
196
|
+
r.code
|
|
193
197
|
)) })
|
|
194
198
|
] });
|
|
195
|
-
},
|
|
196
|
-
const e = /* @__PURE__ */ new Map(), t = /* @__PURE__ */ new Map(),
|
|
197
|
-
const o =
|
|
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
|
-
|
|
208
|
+
r.set(a, g), await o.catch(() => {
|
|
205
209
|
});
|
|
206
210
|
try {
|
|
207
|
-
return await
|
|
211
|
+
return await l();
|
|
208
212
|
} finally {
|
|
209
|
-
u(),
|
|
213
|
+
u(), r.get(a) === g && r.delete(a);
|
|
210
214
|
}
|
|
211
|
-
}, s = async (
|
|
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 = `${
|
|
218
|
+
const o = `${a}:${l}`;
|
|
215
219
|
let u = e.get(o);
|
|
216
220
|
if (!u) {
|
|
217
|
-
if (await window.Translator.availability({ sourceLanguage:
|
|
218
|
-
u = window.Translator.create({ sourceLanguage:
|
|
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:
|
|
225
|
-
if (
|
|
226
|
-
const u = await s(
|
|
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
|
|
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
|
-
},
|
|
242
|
+
}, Q = (e) => e;
|
|
239
243
|
export {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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.
|
|
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": ["
|
|
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"}
|
package/dist/app/types.d.ts.map
DELETED
|
@@ -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"}
|
package/dist/core/html.d.ts.map
DELETED
|
@@ -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"}
|
package/dist/core/types.d.ts.map
DELETED
|
@@ -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"}
|
package/dist/index.d.ts.map
DELETED
|
@@ -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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"}
|