react-email-locale-lab 0.4.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.
@@ -1,6 +1,8 @@
1
1
  import type { TranslationProvider } from './types';
2
2
  type BrowserTranslator = {
3
- translate: (text: string) => Promise<string>;
3
+ translate: (text: string, options?: {
4
+ signal?: AbortSignal;
5
+ }) => Promise<string>;
4
6
  destroy?: () => void;
5
7
  };
6
8
  type TranslatorFactory = {
@@ -22,6 +22,7 @@ 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;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsxs as d, jsx as i } from "react/jsx-runtime";
2
- import { createElement as B, useMemo as R, useState as L, useEffect as S } from "react";
3
- import { renderToStaticMarkup as F } from "react-dom/server";
4
- const _ = ["alt", "title", "aria-label"], O = /* @__PURE__ */ new Set([
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
5
  "ar",
6
6
  "arc",
7
7
  "ckb",
@@ -18,43 +18,43 @@ const _ = ["alt", "title", "aria-label"], O = /* @__PURE__ */ new Set([
18
18
  "ug",
19
19
  "ur",
20
20
  "yi"
21
- ]), H = /* @__PURE__ */ new Set(["adlm", "arab", "hebr", "mand", "nkoo", "rohg", "samr", "syrc", "thaa"]), j = (e) => {
21
+ ]), j = /* @__PURE__ */ new Set(["adlm", "arab", "hebr", "mand", "nkoo", "rohg", "samr", "syrc", "thaa"]), z = (e) => {
22
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 ? H.has(a.toLowerCase()) ? "rtl" : "ltr" : O.has(r) ? "rtl" : "ltr";
24
- }, C = (e) => new RegExp("\\p{L}", "u").test(e), z = (e) => {
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
25
  const t = [], r = e.createTreeWalker(e.body, NodeFilter.SHOW_TEXT);
26
26
  let s = r.nextNode();
27
27
  for (; s; ) {
28
28
  const n = s, a = n.parentElement, o = n.data.trim();
29
- if (a && !["STYLE", "SCRIPT"].includes(a.tagName) && C(o)) {
29
+ if (a && !["STYLE", "SCRIPT"].includes(a.tagName) && U(o)) {
30
30
  const c = n.data.match(/^\s*/)?.[0] ?? "", u = n.data.match(/\s*$/)?.[0] ?? "";
31
- t.push({ value: o, apply: (w) => {
32
- n.data = `${c}${w}${u}`;
31
+ t.push({ value: o, apply: (v) => {
32
+ n.data = `${c}${v}${u}`;
33
33
  } });
34
34
  }
35
35
  s = r.nextNode();
36
36
  }
37
37
  for (const n of e.body.querySelectorAll("*"))
38
- for (const a of _) {
38
+ for (const a of O) {
39
39
  const o = n.getAttribute(a)?.trim();
40
- o && C(o) && t.push({ value: o, apply: (c) => n.setAttribute(a, c) });
40
+ o && U(o) && t.push({ value: o, apply: (c) => n.setAttribute(a, c) });
41
41
  }
42
42
  return t;
43
- }, q = async (e, t, r, s) => {
43
+ }, D = async (e, t, r, s) => {
44
44
  if (r === t) return e;
45
45
  const n = new DOMParser().parseFromString(e, "text/html");
46
- n.documentElement.lang = r, n.documentElement.dir = j(r);
47
- const a = z(n), o = await s(a.map((c) => c.value));
46
+ n.documentElement.lang = r, n.documentElement.dir = z(r);
47
+ const a = q(n), o = await s(a.map((c) => c.value));
48
48
  return a.forEach((c, u) => c.apply(o[u] ?? c.value)), `<!doctype html>${n.documentElement.outerHTML}`;
49
- }, U = async (e) => {
49
+ }, $ = async (e) => {
50
50
  const t = new TextEncoder().encode(e), r = await crypto.subtle.digest("SHA-256", t);
51
51
  return Array.from(new Uint8Array(r).slice(0, 6), (s) => s.toString(16).padStart(2, "0")).join("");
52
- }, D = (e) => {
52
+ }, G = (e) => {
53
53
  const t = new DOMParser().parseFromString(e, "text/html");
54
54
  return Array.from(t.body.querySelectorAll("[style]")).find(
55
55
  (n) => n.style.display === "none" && n.textContent?.trim()
56
56
  )?.textContent?.replace(/[\u00A0\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g, "").trim() || void 0;
57
- }, G = ({ locale: e, preview: t, sourceRevision: r, onRefresh: s }) => /* @__PURE__ */ d("article", { className: "preview-card", children: [
57
+ }, W = ({ locale: e, preview: t, sourceRevision: r, onRefresh: s }) => /* @__PURE__ */ d("article", { className: "preview-card", children: [
58
58
  /* @__PURE__ */ d("header", { className: "preview-header", children: [
59
59
  /* @__PURE__ */ d("div", { children: [
60
60
  /* @__PURE__ */ i("strong", { children: e.label }),
@@ -73,98 +73,100 @@ const _ = ["alt", "title", "aria-label"], O = /* @__PURE__ */ new Set([
73
73
  ] }),
74
74
  /* @__PURE__ */ d("div", { className: "preheader", children: [
75
75
  /* @__PURE__ */ i("span", { children: "Preheader" }),
76
- /* @__PURE__ */ i("strong", { children: D(t.html) ?? "Not set" })
76
+ /* @__PURE__ */ i("strong", { children: G(t.html) ?? "Not set" })
77
77
  ] }),
78
78
  /* @__PURE__ */ i("iframe", { title: `${e.label} preview`, srcDoc: t.html, sandbox: "allow-popups allow-popups-to-escape-sandbox" }),
79
79
  t.error && /* @__PURE__ */ i("div", { className: "preview-error", children: t.error }),
80
80
  t.status !== "source" && /* @__PURE__ */ i("button", { className: "refresh", onClick: s, children: "Regenerate translation" })
81
- ] }), W = (e) => {
81
+ ] }), V = (e) => {
82
82
  if ("render" in e && e.render) return e.render();
83
83
  const t = e.props ?? e.component.PreviewProps ?? {};
84
- return B(e.component, t);
85
- }, $ = "/preview", k = (e = $) => `/${e}`.replace(/\/+/g, "/").replace(/\/$/, "") || $, E = (e, t, r) => {
86
- const s = k(r), a = (e.pathname.startsWith(`${s}/`) ? decodeURIComponent(e.pathname.slice(s.length + 1)) : void 0) || e.searchParams.get("template") || void 0;
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
87
  return a && t.includes(a) ? a : t[0];
88
- }, x = (e, t = 3) => e.searchParams.get("langs")?.split(",").filter(Boolean).slice(0, t) ?? [], I = (e, t, r) => {
88
+ }, I = (e, t = 3) => e.searchParams.get("langs")?.split(",").filter(Boolean).slice(0, t) ?? [], k = (e, t, r) => {
89
89
  const s = new URL(e);
90
- return s.pathname = `${k(r)}/${encodeURIComponent(t)}`, s.searchParams.delete("template"), s;
91
- }, V = (e, t) => {
90
+ return s.pathname = `${M(r)}/${encodeURIComponent(t)}`, s.searchParams.delete("template"), s;
91
+ }, J = (e, t) => {
92
92
  const r = new URL(e);
93
93
  return t.length ? r.searchParams.set("langs", t.join(",")) : r.searchParams.delete("langs"), r;
94
- }, N = 3, J = (e) => {
95
- const t = R(() => Object.keys(e.templates), [e.templates]), [r, s] = L(() => E(new URL(window.location.href), t, e.routeBasePath)), [n, a] = L(() => x(new URL(window.location.href), N)), [o, c] = L({}), [u, w] = L(""), [v, b] = L(0), f = e.templates[r] ?? e.templates[t[0]], p = R(
96
- () => `<!doctype html>${F(W(f))}`,
97
- [f, v]
98
- ), g = e.locales.filter((l) => n.includes(l.code)), A = [e.sourceLocale, ...g];
99
- return S(() => {
100
- const l = E(new URL(window.location.href), t, e.routeBasePath);
101
- window.history.replaceState({}, "", I(new URL(window.location.href), l, e.routeBasePath));
102
- const y = () => {
103
- s(E(new URL(window.location.href), t, e.routeBasePath)), a(x(new URL(window.location.href), N));
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));
104
104
  };
105
- return window.addEventListener("popstate", y), () => window.removeEventListener("popstate", y);
106
- }, [e.routeBasePath, t]), S(() => e.sourceUpdates?.subscribe(() => {
107
- b((l) => l + 1);
108
- }), [e.sourceUpdates]), S(() => {
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
109
  let l = !1;
110
- return U(p).then((y) => {
111
- l || w(y);
110
+ return $(m).then((w) => {
111
+ l || v(w);
112
112
  }), () => {
113
113
  l = !0;
114
114
  };
115
- }, [p]), S(() => {
115
+ }, [m]), P(() => {
116
116
  let l = !1;
117
- return c((h) => Object.fromEntries(A.map((m) => [
118
- m.code,
119
- m.code === e.sourceLocale.code ? { html: p, status: "source" } : { html: h[m.code]?.html ?? p, status: "stale", revision: h[m.code]?.revision }
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 }
120
121
  ]))), (async () => {
121
- for (const h of g) {
122
+ for (const p of f) {
122
123
  if (l) return;
123
- c((m) => ({
124
- ...m,
125
- [h.code]: { ...m[h.code], status: "translating", error: void 0 }
124
+ c((h) => ({
125
+ ...h,
126
+ [p.code]: { ...h[p.code], status: "translating", error: void 0 }
126
127
  }));
127
128
  try {
128
- const m = await q(p, e.sourceLocale.code, h.code, (P) => e.provider.translate({
129
- texts: P,
129
+ const h = await D(m, e.sourceLocale.code, p.code, (E) => e.provider.translate({
130
+ texts: E,
130
131
  sourceLocale: e.sourceLocale.translationCode ?? e.sourceLocale.code,
131
- targetLocale: h.translationCode ?? h.code
132
- })), T = await U(p);
133
- l || c((P) => ({ ...P, [h.code]: { html: m, status: "ready", revision: T } }));
134
- } catch (m) {
135
- l || c((T) => ({
136
- ...T,
137
- [h.code]: {
138
- ...T[h.code],
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],
139
141
  status: "error",
140
- error: m instanceof Error ? m.message : String(m)
142
+ error: h instanceof Error ? h.message : String(h)
141
143
  }
142
144
  }));
143
145
  }
144
146
  }
145
147
  })(), () => {
146
- l = !0;
148
+ l = !0, w.abort();
147
149
  };
148
- }, [p, v, n.join(",")]), {
150
+ }, [m, g, n.join(",")]), {
149
151
  activeLocaleCodes: n,
150
- activeLocales: g,
151
- previewLocales: A,
152
+ activeLocales: f,
153
+ previewLocales: b,
152
154
  previews: o,
153
- refreshPreviews: () => b((l) => l + 1),
155
+ refreshPreviews: () => y((l) => l + 1),
154
156
  selectTemplate: (l) => {
155
- s(l), window.history.pushState({}, "", I(new URL(window.location.href), l, e.routeBasePath));
157
+ s(l), window.history.pushState({}, "", k(new URL(window.location.href), l, e.routeBasePath));
156
158
  },
157
159
  selectedTemplateId: r,
158
- sourceHtml: p,
160
+ sourceHtml: m,
159
161
  sourceRevision: u,
160
162
  templateIds: t,
161
163
  toggleLocale: (l) => {
162
- const y = n.includes(l) ? n.filter((h) => h !== l) : [...n, l].slice(0, N);
163
- a(y), window.history.replaceState({}, "", V(new URL(window.location.href), y));
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));
164
166
  }
165
167
  };
166
- }, re = ({ config: e }) => {
167
- const t = J(e);
168
+ }, ae = ({ config: e }) => {
169
+ const t = Y(e);
168
170
  return /* @__PURE__ */ d("main", { className: "email-lab", children: [
169
171
  /* @__PURE__ */ d("header", { className: "topbar", children: [
170
172
  /* @__PURE__ */ d("div", { className: "topbar-copy", children: [
@@ -213,7 +215,7 @@ const _ = ["alt", "title", "aria-label"], O = /* @__PURE__ */ new Set([
213
215
  /* @__PURE__ */ i("span", { children: "The source template renders immediately; translation starts only on demand." })
214
216
  ] }),
215
217
  /* @__PURE__ */ i("section", { className: "grid", children: t.previewLocales.map((r) => /* @__PURE__ */ i(
216
- G,
218
+ W,
217
219
  {
218
220
  locale: r,
219
221
  preview: t.previews[r.code] ?? { html: t.sourceHtml, status: "stale" },
@@ -223,9 +225,9 @@ const _ = ["alt", "title", "aria-label"], O = /* @__PURE__ */ new Set([
223
225
  r.code
224
226
  )) })
225
227
  ] });
226
- }, M = "react-email-locale-lab:translations:v1", Y = () => {
228
+ }, B = "react-email-locale-lab:translations:v1", K = () => {
227
229
  try {
228
- const e = window.sessionStorage?.getItem(M);
230
+ const e = window.sessionStorage?.getItem(B);
229
231
  if (!e) return /* @__PURE__ */ new Map();
230
232
  const t = JSON.parse(e);
231
233
  return Array.isArray(t) ? new Map(t.filter(
@@ -234,26 +236,26 @@ const _ = ["alt", "title", "aria-label"], O = /* @__PURE__ */ new Set([
234
236
  } catch {
235
237
  return /* @__PURE__ */ new Map();
236
238
  }
237
- }, K = (e) => {
239
+ }, X = (e) => {
238
240
  try {
239
- window.sessionStorage?.setItem(M, JSON.stringify([...e]));
241
+ window.sessionStorage?.setItem(B, JSON.stringify([...e]));
240
242
  } catch {
241
243
  }
242
- }, ae = () => {
243
- const e = /* @__PURE__ */ new Map(), t = Y(), r = /* @__PURE__ */ new Map(), s = async (a, o) => {
244
+ }, se = () => {
245
+ const e = /* @__PURE__ */ new Map(), t = K(), r = /* @__PURE__ */ new Map(), s = async (a, o) => {
244
246
  const c = r.get(a) ?? Promise.resolve();
245
247
  let u = () => {
246
248
  };
247
- const w = new Promise((b) => {
248
- u = b;
249
- }), v = c.catch(() => {
250
- }).then(() => w);
251
- r.set(a, v), await c.catch(() => {
249
+ const v = new Promise((y) => {
250
+ u = y;
251
+ }), g = c.catch(() => {
252
+ }).then(() => v);
253
+ r.set(a, g), await c.catch(() => {
252
254
  });
253
255
  try {
254
256
  return await o();
255
257
  } finally {
256
- u(), r.get(a) === v && r.delete(a);
258
+ u(), r.get(a) === g && r.delete(a);
257
259
  }
258
260
  }, n = async (a, o) => {
259
261
  if (!window.Translator)
@@ -268,28 +270,30 @@ const _ = ["alt", "title", "aria-label"], O = /* @__PURE__ */ new Set([
268
270
  };
269
271
  return {
270
272
  name: "Browser Translator · on-device",
271
- async translate({ texts: a, sourceLocale: o, targetLocale: c }) {
273
+ async translate({ texts: a, sourceLocale: o, targetLocale: c, signal: u }) {
272
274
  if (a.length === 0) return [];
273
- const u = await n(o, c), w = `${o}:${c}`;
274
- return s(w, async () => {
275
- const v = [];
276
- let b = !1;
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;
277
280
  try {
278
- for (const f of a) {
279
- const p = `${w}:${f}`;
280
- let g = t.get(p);
281
- g || (g = await u.translate(f), t.set(p, g), b = !0), v.push(g);
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);
282
286
  }
283
- return v;
287
+ return y;
284
288
  } finally {
285
- b && K(t);
289
+ L && X(t);
286
290
  }
287
291
  });
288
292
  }
289
293
  };
290
- }, se = (e) => e;
294
+ }, ne = (e) => e;
291
295
  export {
292
- re as EmailLabApp,
293
- ae as browserTranslatorProvider,
294
- se as defineEmailLab
296
+ ae as EmailLabApp,
297
+ se as browserTranslatorProvider,
298
+ ne as defineEmailLab
295
299
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email-locale-lab",
3
- "version": "0.4.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",