react-email-locale-lab 0.1.0 → 0.3.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 +48 -7
- 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 +13 -3
- package/dist/core/watch.d.ts +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +75 -69
- package/dist/styles.css +1 -1
- 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
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# React Email Locale Lab
|
|
2
2
|
|
|
3
|
-
React Email Locale Lab is a
|
|
3
|
+
React Email Locale Lab is a development utility for previewing [React Email](https://react.email/) templates in multiple languages while editing them.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[Live demo](https://react-email-locale-lab-integration.vercel.app/) · [Integration example](https://github.com/hducati/react-email-locale-lab-integration-example)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
I developed this library after encountering this need while working at an organization that used React Email templates in multiple languages. The templates were usually created first in English, but we also needed to understand how the same layout behaved in languages such as German and Russian, where translated text could change line wrapping, spacing and element dimensions.
|
|
8
|
+
|
|
9
|
+
During local development, the library renders a React Email template and generates on-demand previews for the selected languages. When the source template changes, Vite refreshes it and the selected previews are generated again, allowing the versions to be inspected side by side in near real time.
|
|
10
|
+
|
|
11
|
+
The generated translations are intended only for visual inspection of the templates. The library does not replace the localization workflow or the reviewed translations used by the application.
|
|
8
12
|
|
|
9
13
|
## Install
|
|
10
14
|
|
|
@@ -12,7 +16,7 @@ This project exists to test the technical and product viability of that workflow
|
|
|
12
16
|
pnpm add -D react-email-locale-lab
|
|
13
17
|
```
|
|
14
18
|
|
|
15
|
-
The package
|
|
19
|
+
The package supports React and React DOM 18 or 19 as peer dependencies.
|
|
16
20
|
|
|
17
21
|
Create a configuration in the consuming project:
|
|
18
22
|
|
|
@@ -26,11 +30,12 @@ import 'react-email-locale-lab/styles.css';
|
|
|
26
30
|
|
|
27
31
|
const config = defineEmailLab({
|
|
28
32
|
routeBasePath: '/preview',
|
|
33
|
+
watchPaths: ['src/emails'],
|
|
29
34
|
sourceLocale: { code: 'en', label: 'English' },
|
|
30
35
|
locales: [{ code: 'de', label: 'Deutsch' }],
|
|
31
36
|
provider: browserTranslatorProvider(),
|
|
32
37
|
templates: {
|
|
33
|
-
welcome: { name: 'Welcome',
|
|
38
|
+
welcome: { name: 'Welcome', component: WelcomeEmail },
|
|
34
39
|
},
|
|
35
40
|
});
|
|
36
41
|
|
|
@@ -55,6 +60,7 @@ Open `http://localhost:4173/preview/welcome`, select up to three languages, then
|
|
|
55
60
|
```tsx
|
|
56
61
|
export default defineEmailLab({
|
|
57
62
|
routeBasePath: '/preview',
|
|
63
|
+
watchPaths: ['src/emails'],
|
|
58
64
|
sourceLocale: { code: 'en', label: 'English' },
|
|
59
65
|
locales: [
|
|
60
66
|
{ code: 'de', label: 'Deutsch' },
|
|
@@ -62,11 +68,46 @@ export default defineEmailLab({
|
|
|
62
68
|
],
|
|
63
69
|
provider: browserTranslatorProvider(),
|
|
64
70
|
templates: {
|
|
65
|
-
welcome: { name: 'Welcome',
|
|
71
|
+
welcome: { name: 'Welcome', component: WelcomeEmail },
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
When a template declares React Email `PreviewProps`, the lab uses them automatically:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
WelcomeEmail.PreviewProps = { customerName: 'Taylor' };
|
|
80
|
+
|
|
81
|
+
templates: {
|
|
82
|
+
welcome: { name: 'Welcome', component: WelcomeEmail },
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Set `props` on the template entry to override `PreviewProps`. A custom `render` function remains available for templates that need bespoke setup.
|
|
87
|
+
|
|
88
|
+
## Template locations and source watching
|
|
89
|
+
|
|
90
|
+
Templates can be imported from anywhere in the consuming project. They do not need to live in `src/emails` or follow a specific folder structure.
|
|
91
|
+
|
|
92
|
+
Use `watchPaths` to select the directories that should trigger preview regeneration during Vite development. Each entry is matched as a path fragment:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
export default defineEmailLab({
|
|
96
|
+
watchPaths: [
|
|
97
|
+
'src/domains/billing/notifications',
|
|
98
|
+
'src/shared/message-templates',
|
|
99
|
+
'packages/transactional-mail',
|
|
100
|
+
],
|
|
101
|
+
templates: {
|
|
102
|
+
invoice: { name: 'Invoice', component: InvoiceEmail },
|
|
103
|
+
welcome: { name: 'Welcome', component: WelcomeEmail },
|
|
66
104
|
},
|
|
105
|
+
// sourceLocale, locales and provider...
|
|
67
106
|
});
|
|
68
107
|
```
|
|
69
108
|
|
|
109
|
+
When `watchPaths` is omitted, the lab reloads for changes to JavaScript and TypeScript modules anywhere in the Vite application. In a larger codebase, configure it to avoid regenerating previews for unrelated source changes.
|
|
110
|
+
|
|
70
111
|
## Template routes
|
|
71
112
|
|
|
72
113
|
Every configured template has a stable preview route:
|
|
@@ -97,4 +138,4 @@ The included provider uses the browser's built-in Translator API. Language packs
|
|
|
97
138
|
|
|
98
139
|
## Current scope
|
|
99
140
|
|
|
100
|
-
React Email Locale Lab is currently a
|
|
141
|
+
React Email Locale Lab is currently a development-time proof of concept. It automatically translates rendered React Email content for visual preview only; it is not a production localization system or a replacement for testing in real email clients. Future iterations may protect placeholders, improve RTL coverage, expose language-pack progress and support additional translation providers.
|
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;
|
|
@@ -23,6 +33,6 @@ export type EmailLabConfig = {
|
|
|
23
33
|
templates: Record<string, EmailTemplate>;
|
|
24
34
|
provider: TranslationProvider;
|
|
25
35
|
routeBasePath?: string;
|
|
36
|
+
watchPaths?: string[];
|
|
26
37
|
};
|
|
27
38
|
export declare const defineEmailLab: (config: EmailLabConfig) => EmailLabConfig;
|
|
28
|
-
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const shouldReloadForUpdates: (updatePaths: string[], watchPaths?: string[]) => boolean;
|
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,47 +1,47 @@
|
|
|
1
|
-
import { jsxs as d, jsx as
|
|
2
|
-
import { useMemo as A, useState as
|
|
3
|
-
import { renderToStaticMarkup as
|
|
4
|
-
const
|
|
1
|
+
import { jsxs as d, jsx as c } from "react/jsx-runtime";
|
|
2
|
+
import { createElement as I, useMemo as A, useState as L, 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
5
|
const t = [], a = e.createTreeWalker(e.body, NodeFilter.SHOW_TEXT);
|
|
6
6
|
let n = a.nextNode();
|
|
7
7
|
for (; n; ) {
|
|
8
|
-
const s = n, r = s.parentElement,
|
|
9
|
-
if (r && !["STYLE", "SCRIPT"].includes(r.tagName) && $(
|
|
8
|
+
const s = n, r = s.parentElement, l = s.data.trim();
|
|
9
|
+
if (r && !["STYLE", "SCRIPT"].includes(r.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
15
|
n = a.nextNode();
|
|
16
16
|
}
|
|
17
17
|
for (const s of e.body.querySelectorAll("*"))
|
|
18
|
-
for (const r of
|
|
19
|
-
const
|
|
20
|
-
|
|
18
|
+
for (const r of k) {
|
|
19
|
+
const l = s.getAttribute(r)?.trim();
|
|
20
|
+
l && $(l) && t.push({ value: l, apply: (o) => s.setAttribute(r, o) });
|
|
21
21
|
}
|
|
22
22
|
return t;
|
|
23
|
-
},
|
|
23
|
+
}, H = async (e, t, a, n) => {
|
|
24
24
|
if (a === t) return e;
|
|
25
25
|
const s = new DOMParser().parseFromString(e, "text/html");
|
|
26
26
|
s.documentElement.lang = a;
|
|
27
|
-
const r =
|
|
28
|
-
return r.forEach((o, u) => o.apply(
|
|
27
|
+
const r = j(s), l = await n(r.map((o) => o.value));
|
|
28
|
+
return r.forEach((o, u) => o.apply(l[u] ?? o.value)), `<!doctype html>${s.documentElement.outerHTML}`;
|
|
29
29
|
}, U = async (e) => {
|
|
30
30
|
const t = new TextEncoder().encode(e), a = await crypto.subtle.digest("SHA-256", t);
|
|
31
31
|
return Array.from(new Uint8Array(a).slice(0, 6), (n) => n.toString(16).padStart(2, "0")).join("");
|
|
32
|
-
},
|
|
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: a, 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
|
] }),
|
|
@@ -52,32 +52,36 @@ const M = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
|
|
|
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
|
-
] }),
|
|
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, N = (e, t, a) => {
|
|
62
66
|
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
67
|
return r && t.includes(r) ? r : t[0];
|
|
64
68
|
}, C = (e, t = 3) => e.searchParams.get("langs")?.split(",").filter(Boolean).slice(0, t) ?? [], B = (e, t, a) => {
|
|
65
69
|
const n = new URL(e);
|
|
66
70
|
return n.pathname = `${F(a)}/${encodeURIComponent(t)}`, n.searchParams.delete("template"), n;
|
|
67
|
-
},
|
|
71
|
+
}, q = (e, t) => {
|
|
68
72
|
const a = new URL(e);
|
|
69
73
|
return t.length ? a.searchParams.set("langs", t.join(",")) : a.searchParams.delete("langs"), a;
|
|
70
|
-
},
|
|
71
|
-
const t = A(() => Object.keys(e.templates), [e.templates]), [a, n] =
|
|
74
|
+
}, R = 3, D = (e) => {
|
|
75
|
+
const t = A(() => Object.keys(e.templates), [e.templates]), [a, n] = L(() => N(new URL(window.location.href), t, e.routeBasePath)), [s, r] = L(() => C(new URL(window.location.href), R)), [l, o] = L({}), [u, w] = L(""), [g, y] = L(0), b = e.templates[a] ?? e.templates[t[0]], h = A(() => `<!doctype html>${M(z(b))}`, [b]), P = e.locales.filter((i) => s.includes(i.code)), S = [e.sourceLocale, ...P];
|
|
72
76
|
return T(() => {
|
|
73
|
-
const i =
|
|
77
|
+
const i = N(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(
|
|
80
|
+
n(N(new URL(window.location.href), t, e.routeBasePath)), r(C(new URL(window.location.href), R));
|
|
77
81
|
};
|
|
78
82
|
return window.addEventListener("popstate", v), () => window.removeEventListener("popstate", v);
|
|
79
83
|
}, [e.routeBasePath, t]), T(() => () => {
|
|
80
|
-
}, []), T(() => {
|
|
84
|
+
}, [e.watchPaths]), T(() => {
|
|
81
85
|
let i = !1;
|
|
82
86
|
return U(h).then((v) => {
|
|
83
87
|
i || w(v);
|
|
@@ -86,7 +90,7 @@ const M = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
|
|
|
86
90
|
};
|
|
87
91
|
}, [h]), T(() => {
|
|
88
92
|
let i = !1;
|
|
89
|
-
return o((m) => Object.fromEntries(
|
|
93
|
+
return o((m) => Object.fromEntries(S.map((p) => [
|
|
90
94
|
p.code,
|
|
91
95
|
p.code === e.sourceLocale.code ? { html: h, status: "source" } : { html: m[p.code]?.html ?? h, status: "stale", revision: m[p.code]?.revision }
|
|
92
96
|
]))), (async () => {
|
|
@@ -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
|
|
@@ -120,8 +124,8 @@ const M = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
|
|
|
120
124
|
}, [h, g, s.join(",")]), {
|
|
121
125
|
activeLocaleCodes: s,
|
|
122
126
|
activeLocales: P,
|
|
123
|
-
previewLocales:
|
|
124
|
-
previews:
|
|
127
|
+
previewLocales: S,
|
|
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));
|
|
@@ -131,22 +135,25 @@ const M = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
|
|
|
131
135
|
sourceRevision: u,
|
|
132
136
|
templateIds: t,
|
|
133
137
|
toggleLocale: (i) => {
|
|
134
|
-
const v = s.includes(i) ? s.filter((m) => m !== i) : [...s, i].slice(0,
|
|
135
|
-
r(v), window.history.replaceState({}, "",
|
|
138
|
+
const v = s.includes(i) ? s.filter((m) => m !== i) : [...s, i].slice(0, R);
|
|
139
|
+
r(v), window.history.replaceState({}, "", q(new URL(window.location.href), v));
|
|
136
140
|
}
|
|
137
141
|
};
|
|
138
|
-
},
|
|
139
|
-
const t =
|
|
140
|
-
return /* @__PURE__ */ d("main", { children: [
|
|
142
|
+
}, J = ({ config: e }) => {
|
|
143
|
+
const t = D(e);
|
|
144
|
+
return /* @__PURE__ */ d("main", { className: "email-lab", children: [
|
|
141
145
|
/* @__PURE__ */ d("header", { className: "topbar", children: [
|
|
142
|
-
/* @__PURE__ */ d("div", { children: [
|
|
143
|
-
/* @__PURE__ */
|
|
144
|
-
|
|
146
|
+
/* @__PURE__ */ d("div", { className: "topbar-copy", children: [
|
|
147
|
+
/* @__PURE__ */ d("div", { className: "brand", children: [
|
|
148
|
+
/* @__PURE__ */ c("span", { className: "brand-mark" }),
|
|
149
|
+
/* @__PURE__ */ c("p", { className: "eyebrow", children: "React Email Locale Lab" })
|
|
150
|
+
] }),
|
|
151
|
+
/* @__PURE__ */ c("h1", { children: "See every language while you build." })
|
|
145
152
|
] }),
|
|
146
153
|
/* @__PURE__ */ d("div", { className: "controls", children: [
|
|
147
154
|
/* @__PURE__ */ d("label", { children: [
|
|
148
155
|
"Template",
|
|
149
|
-
/* @__PURE__ */
|
|
156
|
+
/* @__PURE__ */ c("select", { value: t.selectedTemplateId, onChange: (a) => t.selectTemplate(a.target.value), children: t.templateIds.map((a) => /* @__PURE__ */ c("option", { value: a, children: e.templates[a].name }, a)) })
|
|
150
157
|
] }),
|
|
151
158
|
/* @__PURE__ */ d("span", { className: "provider", children: [
|
|
152
159
|
"Provider: ",
|
|
@@ -156,17 +163,17 @@ const M = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
|
|
|
156
163
|
] }),
|
|
157
164
|
/* @__PURE__ */ d("section", { className: "locale-picker", children: [
|
|
158
165
|
/* @__PURE__ */ d("div", { children: [
|
|
159
|
-
/* @__PURE__ */
|
|
160
|
-
/* @__PURE__ */
|
|
166
|
+
/* @__PURE__ */ c("strong", { children: "Preview languages" }),
|
|
167
|
+
/* @__PURE__ */ c("span", { children: "Select up to three. Language packs load only when selected." })
|
|
161
168
|
] }),
|
|
162
|
-
/* @__PURE__ */
|
|
169
|
+
/* @__PURE__ */ c("div", { className: "locale-options", children: e.locales.map((a) => /* @__PURE__ */ d(
|
|
163
170
|
"button",
|
|
164
171
|
{
|
|
165
172
|
className: t.activeLocaleCodes.includes(a.code) ? "locale-active" : "",
|
|
166
173
|
disabled: !t.activeLocaleCodes.includes(a.code) && t.activeLocaleCodes.length >= 3,
|
|
167
174
|
onClick: () => t.toggleLocale(a.code),
|
|
168
175
|
children: [
|
|
169
|
-
/* @__PURE__ */
|
|
176
|
+
/* @__PURE__ */ c("i", {}),
|
|
170
177
|
a.label
|
|
171
178
|
]
|
|
172
179
|
},
|
|
@@ -174,15 +181,15 @@ const M = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
|
|
|
174
181
|
)) })
|
|
175
182
|
] }),
|
|
176
183
|
/* @__PURE__ */ d("section", { className: "notice", children: [
|
|
177
|
-
/* @__PURE__ */
|
|
184
|
+
/* @__PURE__ */ c("span", { className: "live-dot" }),
|
|
178
185
|
"Watching source changes through Vite HMR."
|
|
179
186
|
] }),
|
|
180
187
|
t.activeLocales.length === 0 && /* @__PURE__ */ d("section", { className: "empty-state", children: [
|
|
181
|
-
/* @__PURE__ */
|
|
182
|
-
/* @__PURE__ */
|
|
188
|
+
/* @__PURE__ */ c("strong", { children: "Select a language to generate a preview." }),
|
|
189
|
+
/* @__PURE__ */ c("span", { children: "The source template renders immediately; translation starts only on demand." })
|
|
183
190
|
] }),
|
|
184
|
-
/* @__PURE__ */
|
|
185
|
-
|
|
191
|
+
/* @__PURE__ */ c("section", { className: "grid", children: t.previewLocales.map((a) => /* @__PURE__ */ c(
|
|
192
|
+
_,
|
|
186
193
|
{
|
|
187
194
|
locale: a,
|
|
188
195
|
preview: t.previews[a.code] ?? { html: t.sourceHtml, status: "stale" },
|
|
@@ -192,8 +199,8 @@ const M = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
|
|
|
192
199
|
a.code
|
|
193
200
|
)) })
|
|
194
201
|
] });
|
|
195
|
-
},
|
|
196
|
-
const e = /* @__PURE__ */ new Map(), t = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Map(), n = async (r,
|
|
202
|
+
}, K = () => {
|
|
203
|
+
const e = /* @__PURE__ */ new Map(), t = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Map(), n = async (r, l) => {
|
|
197
204
|
const o = a.get(r) ?? Promise.resolve();
|
|
198
205
|
let u = () => {
|
|
199
206
|
};
|
|
@@ -204,41 +211,40 @@ const M = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
|
|
|
204
211
|
a.set(r, g), await o.catch(() => {
|
|
205
212
|
});
|
|
206
213
|
try {
|
|
207
|
-
return await
|
|
214
|
+
return await l();
|
|
208
215
|
} finally {
|
|
209
216
|
u(), a.get(r) === g && a.delete(r);
|
|
210
217
|
}
|
|
211
|
-
}, s = async (r,
|
|
218
|
+
}, s = async (r, l) => {
|
|
212
219
|
if (!window.Translator)
|
|
213
220
|
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}:${
|
|
221
|
+
const o = `${r}:${l}`;
|
|
215
222
|
let u = e.get(o);
|
|
216
223
|
if (!u) {
|
|
217
|
-
if (await window.Translator.availability({ sourceLanguage: r, targetLanguage:
|
|
218
|
-
u = window.Translator.create({ sourceLanguage: r, targetLanguage:
|
|
224
|
+
if (await window.Translator.availability({ sourceLanguage: r, targetLanguage: l }) === "unavailable") throw new Error(`The browser cannot translate ${r} → ${l}.`);
|
|
225
|
+
u = window.Translator.create({ sourceLanguage: r, targetLanguage: l }), e.set(o, u);
|
|
219
226
|
}
|
|
220
227
|
return u;
|
|
221
228
|
};
|
|
222
229
|
return {
|
|
223
230
|
name: "Browser Translator · on-device",
|
|
224
|
-
async translate({ texts: r, sourceLocale:
|
|
231
|
+
async translate({ texts: r, sourceLocale: l, targetLocale: o }) {
|
|
225
232
|
if (r.length === 0) return [];
|
|
226
|
-
const u = await s(
|
|
233
|
+
const u = await s(l, o), w = `${l}:${o}`;
|
|
227
234
|
return n(w, async () => {
|
|
228
235
|
const g = [];
|
|
229
236
|
for (const y of r) {
|
|
230
|
-
const
|
|
231
|
-
let h = t.get(
|
|
232
|
-
h || (h = await u.translate(y), t.set(
|
|
237
|
+
const b = `${w}:${y}`;
|
|
238
|
+
let h = t.get(b);
|
|
239
|
+
h || (h = await u.translate(y), t.set(b, h)), g.push(h);
|
|
233
240
|
}
|
|
234
241
|
return g;
|
|
235
242
|
});
|
|
236
243
|
}
|
|
237
244
|
};
|
|
238
|
-
},
|
|
245
|
+
}, Q = (e) => e;
|
|
239
246
|
export {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
247
|
+
J as EmailLabApp,
|
|
248
|
+
K as browserTranslatorProvider,
|
|
249
|
+
Q as defineEmailLab
|
|
243
250
|
};
|
|
244
|
-
//# sourceMappingURL=index.js.map
|
package/dist/styles.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
:
|
|
1
|
+
.email-lab{--lab-bg: #050505;--lab-panel: #0a0a0a;--lab-panel-raised: #111111;--lab-border: #262626;--lab-border-strong: #3a3a3a;--lab-text: #ededed;--lab-muted: #8f8f8f;--lab-dim: #666666;--lab-accent: #ffffff;--lab-success: #45d483;--lab-warning: #f5a524;--lab-error: #ff6369;background:radial-gradient(circle at 50% -18%,rgba(255,255,255,.09),transparent 30rem),var(--lab-bg);color-scheme:dark;color:var(--lab-text);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-synthesis:none;min-height:100vh;padding:40px clamp(18px,3vw,56px) 72px}.email-lab,.email-lab *{box-sizing:border-box}.email-lab button,.email-lab select{font:inherit}.email-lab .topbar{align-items:end;display:flex;gap:32px;justify-content:space-between;margin:0 auto;max-width:2200px}.email-lab .topbar-copy{max-width:760px}.email-lab .brand{align-items:center;display:flex;gap:9px;margin-bottom:14px}.email-lab .brand-mark{border-bottom:8px solid #fff;border-left:6px solid transparent;border-right:6px solid transparent;display:block;filter:drop-shadow(0 0 10px rgba(255,255,255,.35));height:0;width:0}.email-lab .eyebrow{color:#b8b8b8;font-size:12px;font-weight:600;letter-spacing:.08em;margin:0;text-transform:uppercase}.email-lab h1{font-size:clamp(32px,4vw,58px);font-weight:560;letter-spacing:-.055em;line-height:.98;margin:0;text-wrap:balance}.email-lab .controls{align-items:end;display:flex;flex:0 0 auto;gap:10px}.email-lab label{color:var(--lab-muted);display:grid;font-size:11px;font-weight:550;gap:7px}.email-lab select{background:#0d0d0d;border:1px solid var(--lab-border);border-radius:7px;color:var(--lab-text);min-width:220px;outline:none;padding:10px 34px 10px 12px;transition:border-color .16s ease,background-color .16s ease,box-shadow .16s ease}.email-lab select:hover,.email-lab select:focus-visible{background:#121212;border-color:var(--lab-border-strong);box-shadow:0 0 0 3px #ffffff0d}.email-lab .provider{background:#111;border:1px solid var(--lab-border);border-radius:999px;color:#b5b5b5;font-size:11px;font-weight:550;padding:10px 13px;white-space:nowrap}.email-lab .locale-picker,.email-lab .notice,.email-lab .grid,.email-lab .empty-state{margin-left:auto;margin-right:auto;max-width:2200px}.email-lab .locale-picker{align-items:start;background:#0c0c0ce0;border:1px solid var(--lab-border);border-radius:10px;display:grid;gap:24px;grid-template-columns:minmax(180px,.3fr) minmax(0,1fr);margin-top:34px;padding:16px}.email-lab .locale-picker strong,.email-lab .locale-picker span{display:block}.email-lab .locale-picker strong{font-size:13px;font-weight:580}.email-lab .locale-picker span{color:var(--lab-muted);font-size:11px;line-height:1.45;margin-top:4px}.email-lab .locale-options{display:flex;flex-wrap:wrap;gap:7px;justify-content:flex-end;max-height:118px;overflow:auto;padding:1px 3px 3px 1px;scrollbar-color:#333 transparent;scrollbar-width:thin}.email-lab .locale-options button{align-items:center;background:#111;border:1px solid var(--lab-border);border-radius:6px;color:#a8a8a8;cursor:pointer;display:flex;font-size:11px;font-weight:550;gap:7px;padding:7px 10px;transition:background-color .14s ease,border-color .14s ease,color .14s ease,transform .14s ease}.email-lab .locale-options button:hover:not(:disabled){background:#181818;border-color:#444;color:#fff;transform:translateY(-1px)}.email-lab .locale-options button i{border:1px solid #555;border-radius:50%;height:7px;transition:background-color .14s ease,border-color .14s ease,box-shadow .14s ease;width:7px}.email-lab .locale-options button.locale-active{background:#f2f2f2;border-color:#fff;color:#0a0a0a}.email-lab .locale-options button.locale-active i{background:#0a0a0a;border-color:#0a0a0a;box-shadow:0 0 0 2px #0000001f}.email-lab .locale-options button:disabled{cursor:not-allowed;opacity:.35}.email-lab .notice{align-items:center;border-bottom:1px solid var(--lab-border);color:var(--lab-muted);display:flex;font-size:11px;margin-bottom:18px;padding:14px 2px}.email-lab .live-dot{animation:lab-pulse 1.8s ease-in-out infinite;background:var(--lab-success);border-radius:50%;box-shadow:0 0 10px #45d4838c;height:7px;margin-right:9px;width:7px}.email-lab .grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(min(100%,410px),1fr))}.email-lab .preview-card{animation:lab-enter .32s ease both;background:var(--lab-panel);border:1px solid var(--lab-border);border-radius:10px;box-shadow:0 16px 50px #0000003d;min-width:0;overflow:hidden;position:relative;transition:border-color .18s ease,box-shadow .18s ease,transform .18s ease}.email-lab .preview-card:hover{border-color:#393939;box-shadow:0 20px 65px #00000057;transform:translateY(-2px)}.email-lab .preview-header{align-items:center;border-bottom:1px solid var(--lab-border);display:flex;justify-content:space-between;padding:13px 14px 11px}.email-lab .preview-header strong{display:block;font-size:13px;font-weight:580}.email-lab .preview-header span{color:var(--lab-dim);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px}.email-lab .status{align-items:center;border:1px solid currentColor;border-radius:999px;display:flex;font-size:9px;font-weight:650;gap:6px;letter-spacing:.045em;opacity:.88;padding:4px 7px;text-transform:uppercase}.email-lab .status i{background:currentColor;border-radius:50%;height:5px;width:5px}.email-lab .status-source,.email-lab .status-ready{color:var(--lab-success)}.email-lab .status-stale{color:var(--lab-warning)}.email-lab .status-translating{color:#8da4ff}.email-lab .status-error{color:var(--lab-error)}.email-lab .status-translating i{animation:lab-pulse .8s ease-in-out infinite}.email-lab .revision{background:#080808;border-bottom:1px solid #1b1b1b;color:#5f5f5f;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:9px;padding:7px 14px}.email-lab .preheader{align-items:baseline;border-bottom:1px solid var(--lab-border);display:flex;gap:9px;min-width:0;padding:9px 14px}.email-lab .preheader span{color:#666;flex:0 0 auto;font-size:9px;font-weight:650;letter-spacing:.08em;text-transform:uppercase}.email-lab .preheader strong{color:#aaa;font-size:11px;font-weight:480;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.email-lab iframe{background:#e9e9e9;border:0;display:block;height:620px;width:100%}.email-lab .refresh{-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);background:#0a0a0ae0;border:1px solid #3a3a3a;border-radius:6px;bottom:11px;color:#d3d3d3;cursor:pointer;font-size:10px;font-weight:580;padding:7px 9px;position:absolute;right:11px;transition:background-color .14s ease,border-color .14s ease,color .14s ease}.email-lab .refresh:hover{background:#fff;border-color:#fff;color:#000}.email-lab .preview-error{background:#23080af5;border-top:1px solid #5c2227;bottom:0;color:#ff969a;font-size:11px;left:0;line-height:1.45;padding:11px 128px 11px 13px;position:absolute;right:0}.email-lab .empty-state{align-items:center;background:linear-gradient(180deg,#0d0d0d,#080808);border:1px dashed #303030;border-radius:10px;display:flex;flex-direction:column;justify-content:center;min-height:230px;text-align:center}.email-lab .empty-state strong{font-size:14px;font-weight:580}.email-lab .empty-state span{color:var(--lab-muted);font-size:12px;margin-top:7px}@keyframes lab-pulse{50%{opacity:.35;transform:scale(.82)}}@keyframes lab-enter{0%{opacity:0;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}}@media(min-width:1500px){.email-lab .grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:2050px){.email-lab .grid{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(max-width:900px){.email-lab{padding-top:26px}.email-lab .topbar{align-items:start;flex-direction:column}.email-lab .controls{align-items:stretch;flex-direction:column;width:100%}.email-lab select{width:100%}.email-lab .locale-picker{grid-template-columns:1fr}.email-lab .locale-options{justify-content:flex-start}}@media(prefers-reduced-motion:reduce){.email-lab *,.email-lab *:before,.email-lab *:after{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-email-locale-lab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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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"}
|