react-email-locale-lab 0.3.0 → 0.4.1
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 +58 -29
- package/dist/adapters/vite.d.ts +14 -0
- package/dist/core/browser-translator-provider.d.ts +3 -1
- package/dist/core/html.d.ts +1 -0
- package/dist/core/types.d.ts +5 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +197 -148
- package/dist/vite.js +18 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -10,27 +10,41 @@ During local development, the library renders a React Email template and generat
|
|
|
10
10
|
|
|
11
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.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Quick start
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
pnpm add -D react-email-locale-lab
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The package supports React and React DOM 18 or 19 as
|
|
19
|
+
The package supports React and React DOM 18 or 19 and does not require Vite. The following quick start uses Vite as an optional development host:
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add -D vite
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<!-- email-lab/index.html -->
|
|
27
|
+
<div id="root"></div>
|
|
28
|
+
<script type="module" src="/main.tsx"></script>
|
|
29
|
+
```
|
|
22
30
|
|
|
23
31
|
```tsx
|
|
32
|
+
// email-lab/main.tsx
|
|
33
|
+
/// <reference types="vite/client" />
|
|
34
|
+
|
|
35
|
+
import { createRoot } from 'react-dom/client';
|
|
24
36
|
import {
|
|
25
37
|
browserTranslatorProvider,
|
|
26
38
|
defineEmailLab,
|
|
27
39
|
EmailLabApp,
|
|
28
40
|
} from 'react-email-locale-lab';
|
|
41
|
+
import { viteSourceUpdates } from 'react-email-locale-lab/vite';
|
|
29
42
|
import 'react-email-locale-lab/styles.css';
|
|
43
|
+
import { WelcomeEmail } from '../src/emails/welcome';
|
|
30
44
|
|
|
31
45
|
const config = defineEmailLab({
|
|
32
46
|
routeBasePath: '/preview',
|
|
33
|
-
watchPaths: ['src/emails'],
|
|
47
|
+
sourceUpdates: viteSourceUpdates(import.meta.hot, { watchPaths: ['src/emails'] }),
|
|
34
48
|
sourceLocale: { code: 'en', label: 'English' },
|
|
35
49
|
locales: [{ code: 'de', label: 'Deutsch' }],
|
|
36
50
|
provider: browserTranslatorProvider(),
|
|
@@ -39,28 +53,33 @@ const config = defineEmailLab({
|
|
|
39
53
|
},
|
|
40
54
|
});
|
|
41
55
|
|
|
42
|
-
|
|
56
|
+
createRoot(document.getElementById('root')!).render(<EmailLabApp config={config} />);
|
|
43
57
|
```
|
|
44
58
|
|
|
45
|
-
|
|
59
|
+
Add a script and start the lab:
|
|
46
60
|
|
|
47
|
-
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"scripts": {
|
|
64
|
+
"email:lab": "vite email-lab --port 4174"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
48
68
|
|
|
49
69
|
```bash
|
|
50
|
-
pnpm
|
|
51
|
-
pnpm dev
|
|
70
|
+
pnpm email:lab
|
|
52
71
|
```
|
|
53
72
|
|
|
54
|
-
Open `http://localhost:
|
|
73
|
+
Open `http://localhost:4174/preview/welcome`, select up to three languages, then edit the template. Vite refreshes the selected previews automatically.
|
|
55
74
|
|
|
56
75
|
## Configuration interface
|
|
57
76
|
|
|
58
|
-
|
|
77
|
+
The lab configuration defines only the source locale, available target locales, templates, and translation provider. There are no manually maintained translations. The user selects zero to three languages in the UI; translation is not initialized during app startup.
|
|
59
78
|
|
|
60
79
|
```tsx
|
|
61
80
|
export default defineEmailLab({
|
|
62
81
|
routeBasePath: '/preview',
|
|
63
|
-
watchPaths: ['src/emails'],
|
|
82
|
+
sourceUpdates: viteSourceUpdates(import.meta.hot, { watchPaths: ['src/emails'] }),
|
|
64
83
|
sourceLocale: { code: 'en', label: 'English' },
|
|
65
84
|
locales: [
|
|
66
85
|
{ code: 'de', label: 'Deutsch' },
|
|
@@ -85,28 +104,36 @@ templates: {
|
|
|
85
104
|
|
|
86
105
|
Set `props` on the template entry to override `PreviewProps`. A custom `render` function remains available for templates that need bespoke setup.
|
|
87
106
|
|
|
88
|
-
|
|
107
|
+
### Template requirements
|
|
89
108
|
|
|
90
|
-
|
|
109
|
+
The lab renders templates synchronously to static HTML. Prefer presentation-only email components whose output is determined by props. Pass preview data through `PreviewProps` or `props`; fetch production data before rendering the email.
|
|
91
110
|
|
|
92
|
-
|
|
111
|
+
Data fetching in `useEffect`, async/Server Components, application routes and components that require missing providers may not render as expected. Use `render` to add synchronous providers or wrappers:
|
|
93
112
|
|
|
94
113
|
```tsx
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
'
|
|
98
|
-
|
|
99
|
-
'packages/transactional-mail',
|
|
100
|
-
],
|
|
101
|
-
templates: {
|
|
102
|
-
invoice: { name: 'Invoice', component: InvoiceEmail },
|
|
103
|
-
welcome: { name: 'Welcome', component: WelcomeEmail },
|
|
114
|
+
templates: {
|
|
115
|
+
welcome: {
|
|
116
|
+
name: 'Welcome',
|
|
117
|
+
render: () => <WelcomeEmail />,
|
|
104
118
|
},
|
|
105
|
-
|
|
106
|
-
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Template locations and source updates
|
|
123
|
+
|
|
124
|
+
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.
|
|
125
|
+
|
|
126
|
+
The core library is bundler-agnostic. Without `sourceUpdates`, previews still work and can be regenerated manually. Vite users can opt into HMR updates and restrict them to path fragments:
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import { viteSourceUpdates } from 'react-email-locale-lab/vite';
|
|
130
|
+
|
|
131
|
+
sourceUpdates: viteSourceUpdates(import.meta.hot, {
|
|
132
|
+
watchPaths: ['src/message-templates', 'packages/notifications'],
|
|
133
|
+
}),
|
|
107
134
|
```
|
|
108
135
|
|
|
109
|
-
|
|
136
|
+
Omit `watchPaths` to react to any JavaScript or TypeScript module update. Other tools can implement the small `SourceUpdates` subscription interface and pass it through `sourceUpdates`.
|
|
110
137
|
|
|
111
138
|
## Template routes
|
|
112
139
|
|
|
@@ -132,9 +159,11 @@ nl no pl pt ro ru sk sl sv ta te th tr uk vi zh zh-Hant
|
|
|
132
159
|
|
|
133
160
|
The list is provider-specific and may change. The provider checks every source/target pair with `Translator.availability()` at runtime. Chrome's API currently runs on desktop, downloads language packs on demand and is unavailable on mobile. See the [official Translator API language list](https://developer.chrome.com/docs/ai/translator-api#supported-languages).
|
|
134
161
|
|
|
135
|
-
The provider receives extracted text nodes and translatable attributes (`alt`, `title`, `aria-label`). When source copy changes, every target preview is marked stale and new or changed messages are automatically translated again. Unchanged messages are reused from the
|
|
162
|
+
The provider receives extracted text nodes and translatable attributes (`alt`, `title`, `aria-label`). When source copy changes, every target preview is marked stale and new or changed messages are automatically translated again. Unchanged messages are reused from the session cache.
|
|
163
|
+
|
|
164
|
+
The included provider uses the browser's built-in Translator API. Language packs are lazy-loaded per selected pair, and translated messages are cached by source text and locale in session storage so unchanged copy is reused across Vite reloads. The source template does not leave the browser. In a browser without this API, the preview reports an actionable error instead of downloading a large JavaScript model or freezing startup.
|
|
136
165
|
|
|
137
|
-
|
|
166
|
+
Translated previews set both the BCP 47 `lang` attribute and the corresponding `dir` attribute on the rendered document. Right-to-left locales such as Arabic and Hebrew therefore render with `dir="rtl"`; other target locales explicitly use `dir="ltr"`.
|
|
138
167
|
|
|
139
168
|
## Current scope
|
|
140
169
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SourceUpdates } from '../core/types';
|
|
2
|
+
type ViteUpdatePayload = {
|
|
3
|
+
updates: Array<{
|
|
4
|
+
path: string;
|
|
5
|
+
}>;
|
|
6
|
+
};
|
|
7
|
+
type ViteHotContext = {
|
|
8
|
+
on: (event: 'vite:afterUpdate', listener: (payload: ViteUpdatePayload) => void) => void;
|
|
9
|
+
off: (event: 'vite:afterUpdate', listener: (payload: ViteUpdatePayload) => void) => void;
|
|
10
|
+
};
|
|
11
|
+
export declare const viteSourceUpdates: (hot: ViteHotContext | undefined, options?: {
|
|
12
|
+
watchPaths?: string[];
|
|
13
|
+
}) => SourceUpdates;
|
|
14
|
+
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { TranslationProvider } from './types';
|
|
2
2
|
type BrowserTranslator = {
|
|
3
|
-
translate: (text: string
|
|
3
|
+
translate: (text: string, options?: {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
}) => Promise<string>;
|
|
4
6
|
destroy?: () => void;
|
|
5
7
|
};
|
|
6
8
|
type TranslatorFactory = {
|
package/dist/core/html.d.ts
CHANGED
package/dist/core/types.d.ts
CHANGED
|
@@ -22,17 +22,21 @@ export type TranslationRequest = {
|
|
|
22
22
|
texts: string[];
|
|
23
23
|
sourceLocale: string;
|
|
24
24
|
targetLocale: string;
|
|
25
|
+
signal?: AbortSignal;
|
|
25
26
|
};
|
|
26
27
|
export type TranslationProvider = {
|
|
27
28
|
name: string;
|
|
28
29
|
translate: (request: TranslationRequest) => Promise<string[]>;
|
|
29
30
|
};
|
|
31
|
+
export type SourceUpdates = {
|
|
32
|
+
subscribe: (onUpdate: () => void) => () => void;
|
|
33
|
+
};
|
|
30
34
|
export type EmailLabConfig = {
|
|
31
35
|
sourceLocale: Locale;
|
|
32
36
|
locales: Locale[];
|
|
33
37
|
templates: Record<string, EmailTemplate>;
|
|
34
38
|
provider: TranslationProvider;
|
|
35
39
|
routeBasePath?: string;
|
|
36
|
-
|
|
40
|
+
sourceUpdates?: SourceUpdates;
|
|
37
41
|
};
|
|
38
42
|
export declare const defineEmailLab: (config: EmailLabConfig) => EmailLabConfig;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ import './styles.css';
|
|
|
2
2
|
export { EmailLabApp } from './App';
|
|
3
3
|
export { browserTranslatorProvider } from './core/browser-translator-provider';
|
|
4
4
|
export { defineEmailLab } from './core/types';
|
|
5
|
-
export type { EmailLabConfig, EmailTemplate, Locale, TranslationProvider, TranslationRequest, } from './core/types';
|
|
5
|
+
export type { EmailLabConfig, EmailTemplate, Locale, SourceUpdates, TranslationProvider, TranslationRequest, } from './core/types';
|
package/dist/index.js
CHANGED
|
@@ -1,159 +1,185 @@
|
|
|
1
|
-
import { jsxs as d, jsx as
|
|
2
|
-
import { createElement as
|
|
3
|
-
import { renderToStaticMarkup as
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { jsxs as d, jsx as i } from "react/jsx-runtime";
|
|
2
|
+
import { createElement as F, useMemo as C, useState as T, useEffect as P } from "react";
|
|
3
|
+
import { renderToStaticMarkup as _ } from "react-dom/server";
|
|
4
|
+
const O = ["alt", "title", "aria-label"], H = /* @__PURE__ */ new Set([
|
|
5
|
+
"ar",
|
|
6
|
+
"arc",
|
|
7
|
+
"ckb",
|
|
8
|
+
"dv",
|
|
9
|
+
"fa",
|
|
10
|
+
"he",
|
|
11
|
+
"khw",
|
|
12
|
+
"ks",
|
|
13
|
+
"ku",
|
|
14
|
+
"nqo",
|
|
15
|
+
"ps",
|
|
16
|
+
"sd",
|
|
17
|
+
"syr",
|
|
18
|
+
"ug",
|
|
19
|
+
"ur",
|
|
20
|
+
"yi"
|
|
21
|
+
]), j = /* @__PURE__ */ new Set(["adlm", "arab", "hebr", "mand", "nkoo", "rohg", "samr", "syrc", "thaa"]), z = (e) => {
|
|
22
|
+
const t = e.trim().split(/[-_]/), r = t[0].toLowerCase(), s = t.findIndex((o, c) => c > 0 && /^[a-z0-9]$/i.test(o)), a = t.slice(1, s === -1 ? void 0 : s).find((o) => /^[a-z]{4}$/i.test(o));
|
|
23
|
+
return a ? j.has(a.toLowerCase()) ? "rtl" : "ltr" : H.has(r) ? "rtl" : "ltr";
|
|
24
|
+
}, U = (e) => new RegExp("\\p{L}", "u").test(e), q = (e) => {
|
|
25
|
+
const t = [], r = e.createTreeWalker(e.body, NodeFilter.SHOW_TEXT);
|
|
26
|
+
let s = r.nextNode();
|
|
27
|
+
for (; s; ) {
|
|
28
|
+
const n = s, a = n.parentElement, o = n.data.trim();
|
|
29
|
+
if (a && !["STYLE", "SCRIPT"].includes(a.tagName) && U(o)) {
|
|
30
|
+
const c = n.data.match(/^\s*/)?.[0] ?? "", u = n.data.match(/\s*$/)?.[0] ?? "";
|
|
31
|
+
t.push({ value: o, apply: (v) => {
|
|
32
|
+
n.data = `${c}${v}${u}`;
|
|
13
33
|
} });
|
|
14
34
|
}
|
|
15
|
-
|
|
35
|
+
s = r.nextNode();
|
|
16
36
|
}
|
|
17
|
-
for (const
|
|
18
|
-
for (const
|
|
19
|
-
const
|
|
20
|
-
|
|
37
|
+
for (const n of e.body.querySelectorAll("*"))
|
|
38
|
+
for (const a of O) {
|
|
39
|
+
const o = n.getAttribute(a)?.trim();
|
|
40
|
+
o && U(o) && t.push({ value: o, apply: (c) => n.setAttribute(a, c) });
|
|
21
41
|
}
|
|
22
42
|
return t;
|
|
23
|
-
},
|
|
24
|
-
if (
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
return
|
|
29
|
-
},
|
|
30
|
-
const t = new TextEncoder().encode(e),
|
|
31
|
-
return Array.from(new Uint8Array(
|
|
32
|
-
},
|
|
43
|
+
}, D = async (e, t, r, s) => {
|
|
44
|
+
if (r === t) return e;
|
|
45
|
+
const n = new DOMParser().parseFromString(e, "text/html");
|
|
46
|
+
n.documentElement.lang = r, n.documentElement.dir = z(r);
|
|
47
|
+
const a = q(n), o = await s(a.map((c) => c.value));
|
|
48
|
+
return a.forEach((c, u) => c.apply(o[u] ?? c.value)), `<!doctype html>${n.documentElement.outerHTML}`;
|
|
49
|
+
}, $ = async (e) => {
|
|
50
|
+
const t = new TextEncoder().encode(e), r = await crypto.subtle.digest("SHA-256", t);
|
|
51
|
+
return Array.from(new Uint8Array(r).slice(0, 6), (s) => s.toString(16).padStart(2, "0")).join("");
|
|
52
|
+
}, G = (e) => {
|
|
33
53
|
const t = new DOMParser().parseFromString(e, "text/html");
|
|
34
54
|
return Array.from(t.body.querySelectorAll("[style]")).find(
|
|
35
|
-
(
|
|
55
|
+
(n) => n.style.display === "none" && n.textContent?.trim()
|
|
36
56
|
)?.textContent?.replace(/[\u00A0\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g, "").trim() || void 0;
|
|
37
|
-
},
|
|
57
|
+
}, W = ({ locale: e, preview: t, sourceRevision: r, onRefresh: s }) => /* @__PURE__ */ d("article", { className: "preview-card", children: [
|
|
38
58
|
/* @__PURE__ */ d("header", { className: "preview-header", children: [
|
|
39
59
|
/* @__PURE__ */ d("div", { children: [
|
|
40
|
-
/* @__PURE__ */
|
|
41
|
-
/* @__PURE__ */
|
|
60
|
+
/* @__PURE__ */ i("strong", { children: e.label }),
|
|
61
|
+
/* @__PURE__ */ i("span", { children: e.code })
|
|
42
62
|
] }),
|
|
43
63
|
/* @__PURE__ */ d("div", { className: `status status-${t.status}`, children: [
|
|
44
|
-
/* @__PURE__ */
|
|
64
|
+
/* @__PURE__ */ i("i", {}),
|
|
45
65
|
t.status
|
|
46
66
|
] })
|
|
47
67
|
] }),
|
|
48
68
|
/* @__PURE__ */ d("div", { className: "revision", children: [
|
|
49
69
|
"source ",
|
|
50
|
-
|
|
70
|
+
r || "…",
|
|
51
71
|
" · preview ",
|
|
52
72
|
t.revision ?? "pending"
|
|
53
73
|
] }),
|
|
54
74
|
/* @__PURE__ */ d("div", { className: "preheader", children: [
|
|
55
|
-
/* @__PURE__ */
|
|
56
|
-
/* @__PURE__ */
|
|
75
|
+
/* @__PURE__ */ i("span", { children: "Preheader" }),
|
|
76
|
+
/* @__PURE__ */ i("strong", { children: G(t.html) ?? "Not set" })
|
|
57
77
|
] }),
|
|
58
|
-
/* @__PURE__ */
|
|
59
|
-
t.error && /* @__PURE__ */
|
|
60
|
-
t.status !== "source" && /* @__PURE__ */
|
|
61
|
-
] }),
|
|
78
|
+
/* @__PURE__ */ i("iframe", { title: `${e.label} preview`, srcDoc: t.html, sandbox: "allow-popups allow-popups-to-escape-sandbox" }),
|
|
79
|
+
t.error && /* @__PURE__ */ i("div", { className: "preview-error", children: t.error }),
|
|
80
|
+
t.status !== "source" && /* @__PURE__ */ i("button", { className: "refresh", onClick: s, children: "Regenerate translation" })
|
|
81
|
+
] }), V = (e) => {
|
|
62
82
|
if ("render" in e && e.render) return e.render();
|
|
63
83
|
const t = e.props ?? e.component.PreviewProps ?? {};
|
|
64
|
-
return
|
|
65
|
-
}, x = "/preview",
|
|
66
|
-
const
|
|
67
|
-
return
|
|
68
|
-
},
|
|
69
|
-
const
|
|
70
|
-
return
|
|
71
|
-
},
|
|
72
|
-
const
|
|
73
|
-
return t.length ?
|
|
74
|
-
},
|
|
75
|
-
const t =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
return F(e.component, t);
|
|
85
|
+
}, x = "/preview", M = (e = x) => `/${e}`.replace(/\/+/g, "/").replace(/\/$/, "") || x, A = (e, t, r) => {
|
|
86
|
+
const s = M(r), a = (e.pathname.startsWith(`${s}/`) ? decodeURIComponent(e.pathname.slice(s.length + 1)) : void 0) || e.searchParams.get("template") || void 0;
|
|
87
|
+
return a && t.includes(a) ? a : t[0];
|
|
88
|
+
}, I = (e, t = 3) => e.searchParams.get("langs")?.split(",").filter(Boolean).slice(0, t) ?? [], k = (e, t, r) => {
|
|
89
|
+
const s = new URL(e);
|
|
90
|
+
return s.pathname = `${M(r)}/${encodeURIComponent(t)}`, s.searchParams.delete("template"), s;
|
|
91
|
+
}, J = (e, t) => {
|
|
92
|
+
const r = new URL(e);
|
|
93
|
+
return t.length ? r.searchParams.set("langs", t.join(",")) : r.searchParams.delete("langs"), r;
|
|
94
|
+
}, N = 3, Y = (e) => {
|
|
95
|
+
const t = C(() => Object.keys(e.templates), [e.templates]), [r, s] = T(() => A(new URL(window.location.href), t, e.routeBasePath)), [n, a] = T(() => I(new URL(window.location.href), N)), [o, c] = T({}), [u, v] = T(""), [g, y] = T(0), L = e.templates[r] ?? e.templates[t[0]], m = C(
|
|
96
|
+
() => `<!doctype html>${_(V(L))}`,
|
|
97
|
+
[L, g]
|
|
98
|
+
), f = e.locales.filter((l) => n.includes(l.code)), b = [e.sourceLocale, ...f];
|
|
99
|
+
return P(() => {
|
|
100
|
+
const l = A(new URL(window.location.href), t, e.routeBasePath);
|
|
101
|
+
window.history.replaceState({}, "", k(new URL(window.location.href), l, e.routeBasePath));
|
|
102
|
+
const w = () => {
|
|
103
|
+
s(A(new URL(window.location.href), t, e.routeBasePath)), a(I(new URL(window.location.href), N));
|
|
81
104
|
};
|
|
82
|
-
return window.addEventListener("popstate",
|
|
83
|
-
}, [e.routeBasePath, t]),
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
return window.addEventListener("popstate", w), () => window.removeEventListener("popstate", w);
|
|
106
|
+
}, [e.routeBasePath, t]), P(() => e.sourceUpdates?.subscribe(() => {
|
|
107
|
+
y((l) => l + 1);
|
|
108
|
+
}), [e.sourceUpdates]), P(() => {
|
|
109
|
+
let l = !1;
|
|
110
|
+
return $(m).then((w) => {
|
|
111
|
+
l || v(w);
|
|
88
112
|
}), () => {
|
|
89
|
-
|
|
113
|
+
l = !0;
|
|
90
114
|
};
|
|
91
|
-
}, [
|
|
92
|
-
let
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
115
|
+
}, [m]), P(() => {
|
|
116
|
+
let l = !1;
|
|
117
|
+
const w = new AbortController();
|
|
118
|
+
return c((p) => Object.fromEntries(b.map((h) => [
|
|
119
|
+
h.code,
|
|
120
|
+
h.code === e.sourceLocale.code ? { html: m, status: "source" } : { html: p[h.code]?.html ?? m, status: "stale", revision: p[h.code]?.revision }
|
|
96
121
|
]))), (async () => {
|
|
97
|
-
for (const
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
...
|
|
101
|
-
[
|
|
122
|
+
for (const p of f) {
|
|
123
|
+
if (l) return;
|
|
124
|
+
c((h) => ({
|
|
125
|
+
...h,
|
|
126
|
+
[p.code]: { ...h[p.code], status: "translating", error: void 0 }
|
|
102
127
|
}));
|
|
103
128
|
try {
|
|
104
|
-
const
|
|
129
|
+
const h = await D(m, e.sourceLocale.code, p.code, (E) => e.provider.translate({
|
|
105
130
|
texts: E,
|
|
106
131
|
sourceLocale: e.sourceLocale.translationCode ?? e.sourceLocale.code,
|
|
107
|
-
targetLocale:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
targetLocale: p.translationCode ?? p.code,
|
|
133
|
+
signal: w.signal
|
|
134
|
+
})), S = await $(m);
|
|
135
|
+
l || c((E) => ({ ...E, [p.code]: { html: h, status: "ready", revision: S } }));
|
|
136
|
+
} catch (h) {
|
|
137
|
+
l || c((S) => ({
|
|
138
|
+
...S,
|
|
139
|
+
[p.code]: {
|
|
140
|
+
...S[p.code],
|
|
115
141
|
status: "error",
|
|
116
|
-
error:
|
|
142
|
+
error: h instanceof Error ? h.message : String(h)
|
|
117
143
|
}
|
|
118
144
|
}));
|
|
119
145
|
}
|
|
120
146
|
}
|
|
121
147
|
})(), () => {
|
|
122
|
-
|
|
148
|
+
l = !0, w.abort();
|
|
123
149
|
};
|
|
124
|
-
}, [
|
|
125
|
-
activeLocaleCodes:
|
|
126
|
-
activeLocales:
|
|
127
|
-
previewLocales:
|
|
128
|
-
previews:
|
|
129
|
-
refreshPreviews: () => y((
|
|
130
|
-
selectTemplate: (
|
|
131
|
-
|
|
150
|
+
}, [m, g, n.join(",")]), {
|
|
151
|
+
activeLocaleCodes: n,
|
|
152
|
+
activeLocales: f,
|
|
153
|
+
previewLocales: b,
|
|
154
|
+
previews: o,
|
|
155
|
+
refreshPreviews: () => y((l) => l + 1),
|
|
156
|
+
selectTemplate: (l) => {
|
|
157
|
+
s(l), window.history.pushState({}, "", k(new URL(window.location.href), l, e.routeBasePath));
|
|
132
158
|
},
|
|
133
|
-
selectedTemplateId:
|
|
134
|
-
sourceHtml:
|
|
159
|
+
selectedTemplateId: r,
|
|
160
|
+
sourceHtml: m,
|
|
135
161
|
sourceRevision: u,
|
|
136
162
|
templateIds: t,
|
|
137
|
-
toggleLocale: (
|
|
138
|
-
const
|
|
139
|
-
|
|
163
|
+
toggleLocale: (l) => {
|
|
164
|
+
const w = n.includes(l) ? n.filter((R) => R !== l) : [...n, l].slice(0, N);
|
|
165
|
+
a(w), window.history.replaceState({}, "", J(new URL(window.location.href), w));
|
|
140
166
|
}
|
|
141
167
|
};
|
|
142
|
-
},
|
|
143
|
-
const t =
|
|
168
|
+
}, ae = ({ config: e }) => {
|
|
169
|
+
const t = Y(e);
|
|
144
170
|
return /* @__PURE__ */ d("main", { className: "email-lab", children: [
|
|
145
171
|
/* @__PURE__ */ d("header", { className: "topbar", children: [
|
|
146
172
|
/* @__PURE__ */ d("div", { className: "topbar-copy", children: [
|
|
147
173
|
/* @__PURE__ */ d("div", { className: "brand", children: [
|
|
148
|
-
/* @__PURE__ */
|
|
149
|
-
/* @__PURE__ */
|
|
174
|
+
/* @__PURE__ */ i("span", { className: "brand-mark" }),
|
|
175
|
+
/* @__PURE__ */ i("p", { className: "eyebrow", children: "React Email Locale Lab" })
|
|
150
176
|
] }),
|
|
151
|
-
/* @__PURE__ */
|
|
177
|
+
/* @__PURE__ */ i("h1", { children: "See every language while you build." })
|
|
152
178
|
] }),
|
|
153
179
|
/* @__PURE__ */ d("div", { className: "controls", children: [
|
|
154
180
|
/* @__PURE__ */ d("label", { children: [
|
|
155
181
|
"Template",
|
|
156
|
-
/* @__PURE__ */
|
|
182
|
+
/* @__PURE__ */ i("select", { value: t.selectedTemplateId, onChange: (r) => t.selectTemplate(r.target.value), children: t.templateIds.map((r) => /* @__PURE__ */ i("option", { value: r, children: e.templates[r].name }, r)) })
|
|
157
183
|
] }),
|
|
158
184
|
/* @__PURE__ */ d("span", { className: "provider", children: [
|
|
159
185
|
"Provider: ",
|
|
@@ -163,88 +189,111 @@ const k = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
|
|
|
163
189
|
] }),
|
|
164
190
|
/* @__PURE__ */ d("section", { className: "locale-picker", children: [
|
|
165
191
|
/* @__PURE__ */ d("div", { children: [
|
|
166
|
-
/* @__PURE__ */
|
|
167
|
-
/* @__PURE__ */
|
|
192
|
+
/* @__PURE__ */ i("strong", { children: "Preview languages" }),
|
|
193
|
+
/* @__PURE__ */ i("span", { children: "Select up to three. Language packs load only when selected." })
|
|
168
194
|
] }),
|
|
169
|
-
/* @__PURE__ */
|
|
195
|
+
/* @__PURE__ */ i("div", { className: "locale-options", children: e.locales.map((r) => /* @__PURE__ */ d(
|
|
170
196
|
"button",
|
|
171
197
|
{
|
|
172
|
-
className: t.activeLocaleCodes.includes(
|
|
173
|
-
disabled: !t.activeLocaleCodes.includes(
|
|
174
|
-
onClick: () => t.toggleLocale(
|
|
198
|
+
className: t.activeLocaleCodes.includes(r.code) ? "locale-active" : "",
|
|
199
|
+
disabled: !t.activeLocaleCodes.includes(r.code) && t.activeLocaleCodes.length >= 3,
|
|
200
|
+
onClick: () => t.toggleLocale(r.code),
|
|
175
201
|
children: [
|
|
176
|
-
/* @__PURE__ */
|
|
177
|
-
|
|
202
|
+
/* @__PURE__ */ i("i", {}),
|
|
203
|
+
r.label
|
|
178
204
|
]
|
|
179
205
|
},
|
|
180
|
-
|
|
206
|
+
r.code
|
|
181
207
|
)) })
|
|
182
208
|
] }),
|
|
183
209
|
/* @__PURE__ */ d("section", { className: "notice", children: [
|
|
184
|
-
/* @__PURE__ */
|
|
210
|
+
/* @__PURE__ */ i("span", { className: "live-dot" }),
|
|
185
211
|
"Watching source changes through Vite HMR."
|
|
186
212
|
] }),
|
|
187
213
|
t.activeLocales.length === 0 && /* @__PURE__ */ d("section", { className: "empty-state", children: [
|
|
188
|
-
/* @__PURE__ */
|
|
189
|
-
/* @__PURE__ */
|
|
214
|
+
/* @__PURE__ */ i("strong", { children: "Select a language to generate a preview." }),
|
|
215
|
+
/* @__PURE__ */ i("span", { children: "The source template renders immediately; translation starts only on demand." })
|
|
190
216
|
] }),
|
|
191
|
-
/* @__PURE__ */
|
|
192
|
-
|
|
217
|
+
/* @__PURE__ */ i("section", { className: "grid", children: t.previewLocales.map((r) => /* @__PURE__ */ i(
|
|
218
|
+
W,
|
|
193
219
|
{
|
|
194
|
-
locale:
|
|
195
|
-
preview: t.previews[
|
|
220
|
+
locale: r,
|
|
221
|
+
preview: t.previews[r.code] ?? { html: t.sourceHtml, status: "stale" },
|
|
196
222
|
sourceRevision: t.sourceRevision,
|
|
197
223
|
onRefresh: t.refreshPreviews
|
|
198
224
|
},
|
|
199
|
-
|
|
225
|
+
r.code
|
|
200
226
|
)) })
|
|
201
227
|
] });
|
|
202
|
-
}, K = () => {
|
|
203
|
-
|
|
204
|
-
const
|
|
228
|
+
}, B = "react-email-locale-lab:translations:v1", K = () => {
|
|
229
|
+
try {
|
|
230
|
+
const e = window.sessionStorage?.getItem(B);
|
|
231
|
+
if (!e) return /* @__PURE__ */ new Map();
|
|
232
|
+
const t = JSON.parse(e);
|
|
233
|
+
return Array.isArray(t) ? new Map(t.filter(
|
|
234
|
+
(r) => Array.isArray(r) && r.length === 2 && r.every((s) => typeof s == "string")
|
|
235
|
+
)) : /* @__PURE__ */ new Map();
|
|
236
|
+
} catch {
|
|
237
|
+
return /* @__PURE__ */ new Map();
|
|
238
|
+
}
|
|
239
|
+
}, X = (e) => {
|
|
240
|
+
try {
|
|
241
|
+
window.sessionStorage?.setItem(B, JSON.stringify([...e]));
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
}, se = () => {
|
|
245
|
+
const e = /* @__PURE__ */ new Map(), t = K(), r = /* @__PURE__ */ new Map(), s = async (a, o) => {
|
|
246
|
+
const c = r.get(a) ?? Promise.resolve();
|
|
205
247
|
let u = () => {
|
|
206
248
|
};
|
|
207
|
-
const
|
|
249
|
+
const v = new Promise((y) => {
|
|
208
250
|
u = y;
|
|
209
|
-
}), g =
|
|
210
|
-
}).then(() =>
|
|
211
|
-
|
|
251
|
+
}), g = c.catch(() => {
|
|
252
|
+
}).then(() => v);
|
|
253
|
+
r.set(a, g), await c.catch(() => {
|
|
212
254
|
});
|
|
213
255
|
try {
|
|
214
|
-
return await
|
|
256
|
+
return await o();
|
|
215
257
|
} finally {
|
|
216
|
-
u(),
|
|
258
|
+
u(), r.get(a) === g && r.delete(a);
|
|
217
259
|
}
|
|
218
|
-
},
|
|
260
|
+
}, n = async (a, o) => {
|
|
219
261
|
if (!window.Translator)
|
|
220
262
|
throw new Error("This browser does not support the built-in Translator API. Use a current Chrome build or configure a remote provider.");
|
|
221
|
-
const
|
|
222
|
-
let u = e.get(
|
|
263
|
+
const c = `${a}:${o}`;
|
|
264
|
+
let u = e.get(c);
|
|
223
265
|
if (!u) {
|
|
224
|
-
if (await window.Translator.availability({ sourceLanguage:
|
|
225
|
-
u = window.Translator.create({ sourceLanguage:
|
|
266
|
+
if (await window.Translator.availability({ sourceLanguage: a, targetLanguage: o }) === "unavailable") throw new Error(`The browser cannot translate ${a} → ${o}.`);
|
|
267
|
+
u = window.Translator.create({ sourceLanguage: a, targetLanguage: o }), e.set(c, u);
|
|
226
268
|
}
|
|
227
269
|
return u;
|
|
228
270
|
};
|
|
229
271
|
return {
|
|
230
272
|
name: "Browser Translator · on-device",
|
|
231
|
-
async translate({ texts:
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
273
|
+
async translate({ texts: a, sourceLocale: o, targetLocale: c, signal: u }) {
|
|
274
|
+
if (a.length === 0) return [];
|
|
275
|
+
u?.throwIfAborted();
|
|
276
|
+
const v = await n(o, c), g = `${o}:${c}`;
|
|
277
|
+
return s(g, async () => {
|
|
278
|
+
const y = [];
|
|
279
|
+
let L = !1;
|
|
280
|
+
try {
|
|
281
|
+
for (const m of a) {
|
|
282
|
+
u?.throwIfAborted();
|
|
283
|
+
const f = `${g}:${m}`;
|
|
284
|
+
let b = t.get(f);
|
|
285
|
+
b || (b = await v.translate(m, { signal: u }), t.set(f, b), L = !0), y.push(b);
|
|
286
|
+
}
|
|
287
|
+
return y;
|
|
288
|
+
} finally {
|
|
289
|
+
L && X(t);
|
|
240
290
|
}
|
|
241
|
-
return g;
|
|
242
291
|
});
|
|
243
292
|
}
|
|
244
293
|
};
|
|
245
|
-
},
|
|
294
|
+
}, ne = (e) => e;
|
|
246
295
|
export {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
296
|
+
ae as EmailLabApp,
|
|
297
|
+
se as browserTranslatorProvider,
|
|
298
|
+
ne as defineEmailLab
|
|
250
299
|
};
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const c = /\.[cm]?[jt]sx?(?:\?|$)/, n = (e) => e.replaceAll("\\", "/").replace(/^\.\//, ""), p = (e, r) => {
|
|
2
|
+
const s = e.map(n), t = r?.map(n).filter(Boolean);
|
|
3
|
+
return t?.length ? s.some(
|
|
4
|
+
(a) => t.some((o) => a.includes(o))
|
|
5
|
+
) : s.some((a) => c.test(a));
|
|
6
|
+
}, l = (e, r = {}) => ({
|
|
7
|
+
subscribe(s) {
|
|
8
|
+
if (!e) return () => {
|
|
9
|
+
};
|
|
10
|
+
const t = (a) => {
|
|
11
|
+
p(a.updates.map((o) => o.path), r.watchPaths) && s();
|
|
12
|
+
};
|
|
13
|
+
return e.on("vite:afterUpdate", t), () => e.off("vite:afterUpdate", t);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
export {
|
|
17
|
+
l as viteSourceUpdates
|
|
18
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-email-locale-lab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Real-time multi-language previews for React Email templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
16
|
"import": "./dist/index.js"
|
|
17
17
|
},
|
|
18
|
+
"./vite": {
|
|
19
|
+
"types": "./dist/adapters/vite.d.ts",
|
|
20
|
+
"import": "./dist/vite.js"
|
|
21
|
+
},
|
|
18
22
|
"./styles.css": "./dist/styles.css"
|
|
19
23
|
},
|
|
20
24
|
"sideEffects": ["./dist/styles.css"],
|