livelocale 0.8.0 → 0.9.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.
Files changed (53) hide show
  1. package/README.md +230 -5
  2. package/dist/client/index.cjs +178 -70
  3. package/dist/client/index.cjs.map +1 -1
  4. package/dist/client/index.d.cts +36 -22
  5. package/dist/client/index.d.ts +36 -22
  6. package/dist/client/index.js +177 -70
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/codemod/index.cjs +3057 -152
  9. package/dist/codemod/index.cjs.map +1 -1
  10. package/dist/codemod/index.js +3058 -153
  11. package/dist/codemod/index.js.map +1 -1
  12. package/dist/react/index.cjs +185 -77
  13. package/dist/react/index.cjs.map +1 -1
  14. package/dist/react/index.js +185 -77
  15. package/dist/react/index.js.map +1 -1
  16. package/dist/server/index.cjs +108 -66
  17. package/dist/server/index.cjs.map +1 -1
  18. package/dist/server/index.d.cts +13 -3
  19. package/dist/server/index.d.ts +13 -3
  20. package/dist/server/index.js +107 -66
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/studio/assets/index-Bvbtkry-.css +1 -0
  23. package/dist/studio/assets/index-DC_jRsox.js +13 -0
  24. package/dist/studio/assets/inter-latin-300-normal-BVlfKGgI.woff2 +0 -0
  25. package/dist/studio/assets/inter-latin-300-normal-i8F0SvXL.woff +0 -0
  26. package/dist/studio/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  27. package/dist/studio/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  28. package/dist/studio/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  29. package/dist/studio/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  30. package/dist/studio/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  31. package/dist/studio/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  32. package/dist/studio/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
  33. package/dist/studio/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  34. package/dist/studio/assets/outfit-latin-400-normal-BGsTXAXT.woff2 +0 -0
  35. package/dist/studio/assets/outfit-latin-400-normal-DMwTpYkH.woff +0 -0
  36. package/dist/studio/assets/outfit-latin-500-normal-ClnHRwRh.woff +0 -0
  37. package/dist/studio/assets/outfit-latin-500-normal-DKnIMDSk.woff2 +0 -0
  38. package/dist/studio/assets/outfit-latin-600-normal-B7SfZ07L.woff2 +0 -0
  39. package/dist/studio/assets/outfit-latin-600-normal-BEfTtDA7.woff +0 -0
  40. package/dist/studio/assets/outfit-latin-700-normal-Cu9v6i1X.woff2 +0 -0
  41. package/dist/studio/assets/outfit-latin-700-normal-D4itBLBr.woff +0 -0
  42. package/dist/studio/index.html +3 -6
  43. package/dist/vite/index.cjs +739 -77
  44. package/dist/vite/index.cjs.map +1 -1
  45. package/dist/vite/index.d.cts +22 -2
  46. package/dist/vite/index.d.ts +22 -2
  47. package/dist/vite/index.js +738 -77
  48. package/dist/vite/index.js.map +1 -1
  49. package/dist/{wrap-CsOtHp8r.d.cts → wrap-2qVxLN9Q.d.cts} +10 -18
  50. package/dist/{wrap-CsOtHp8r.d.ts → wrap-2qVxLN9Q.d.ts} +10 -18
  51. package/package.json +4 -1
  52. package/dist/studio/assets/index-D7ifnqy5.js +0 -10
  53. package/dist/studio/assets/index-Zh0KtFUH.css +0 -2
@@ -1,10 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  // src/client/watermark.ts
4
- var DELIM = "\u200D";
4
+ var DELIM = "\u200C";
5
5
  var BIT_0 = "\u200B";
6
- var BIT_1 = "\u200C";
6
+ var BIT_1 = "\u200D";
7
+ var WATERMARK_DELIMITER = DELIM;
7
8
  var ZW_RE = /[\u200B\u200C\u200D]/g;
9
+ var WATERMARK_RE = /\u200C(?:[\u200B\u200D]{8})+\u200C/g;
8
10
  function encodeKey(text, key) {
9
11
  const bytes = new TextEncoder().encode(key);
10
12
  let bits = "";
@@ -51,6 +53,15 @@ function stripWatermark(text) {
51
53
  function sanitizeZeroWidth(text) {
52
54
  return text.replace(ZW_RE, "");
53
55
  }
56
+ function removeWatermarks(text) {
57
+ return text.replace(WATERMARK_RE, "");
58
+ }
59
+ function containsWatermark(text) {
60
+ WATERMARK_RE.lastIndex = 0;
61
+ const found = WATERMARK_RE.test(text);
62
+ WATERMARK_RE.lastIndex = 0;
63
+ return found;
64
+ }
54
65
  function hasWatermark(text) {
55
66
  return text.startsWith(DELIM);
56
67
  }
@@ -147,6 +158,121 @@ function isLiveLocaleEnabled() {
147
158
  return evaluate().mode !== "off";
148
159
  }
149
160
 
161
+ // src/client/iframe-bridge.ts
162
+ var DEFAULT_STUDIO_ORIGIN = "http://localhost:3010";
163
+ var STUDIO_ORIGIN_STORAGE_KEY = "livelocale-studio-origin";
164
+ function toHttpOrigin(value) {
165
+ if (!value) return null;
166
+ try {
167
+ const url = new URL(value);
168
+ return url.protocol === "http:" || url.protocol === "https:" ? url.origin : null;
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+ function readSameOriginParent(win) {
174
+ try {
175
+ const parent = win.parent;
176
+ if (!parent || parent === win) return null;
177
+ const own = toHttpOrigin(win.location.origin);
178
+ if (!own) return null;
179
+ return parent.location.origin === own ? own : null;
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+ function resolveStudioOrigin(search, storage, sameOriginParent) {
185
+ const fromParent = toHttpOrigin(sameOriginParent);
186
+ if (fromParent) return fromParent;
187
+ const fromQuery = toHttpOrigin(new URLSearchParams(search).get("livelocale-origin"));
188
+ if (fromQuery) {
189
+ try {
190
+ storage?.setItem(STUDIO_ORIGIN_STORAGE_KEY, fromQuery);
191
+ } catch {
192
+ }
193
+ return fromQuery;
194
+ }
195
+ let remembered = null;
196
+ try {
197
+ remembered = toHttpOrigin(storage?.getItem(STUDIO_ORIGIN_STORAGE_KEY));
198
+ } catch {
199
+ }
200
+ return remembered ?? DEFAULT_STUDIO_ORIGIN;
201
+ }
202
+ var MAX_VISIBLE_TEXTS = 100;
203
+ var MAX_VISIBLE_TEXT_LENGTH = 200;
204
+ var KEY_PATTERN = /^[a-zA-Z0-9._-]+$/;
205
+ var IframeBridge = class {
206
+ allowedOrigin;
207
+ constructor(allowedOrigin) {
208
+ this.allowedOrigin = allowedOrigin;
209
+ }
210
+ /**
211
+ * Sends a message from the iframe client to the parent Studio window.
212
+ */
213
+ sendToParent(type, payload) {
214
+ if (typeof window === "undefined" || !window.parent || window.parent === window) {
215
+ return;
216
+ }
217
+ if (type === "ELEMENT_SELECTED") {
218
+ const p = payload;
219
+ if (!p || typeof p.key !== "string" || p.key.trim() === "" || !/^[a-zA-Z0-9._-]+$/.test(p.key) || typeof p.currentValue !== "string") {
220
+ return;
221
+ }
222
+ }
223
+ if (type === "VISIBLE_KEYS_CHANGED") {
224
+ if (!Array.isArray(payload)) return;
225
+ const validKeys = payload.filter(
226
+ (k) => typeof k === "string" && /^[a-zA-Z0-9._-]+$/.test(k)
227
+ );
228
+ payload = validKeys;
229
+ }
230
+ if (type === "VISIBLE_TEXTS_CHANGED") {
231
+ if (!Array.isArray(payload)) return;
232
+ const texts = [];
233
+ for (const entry of payload) {
234
+ if (texts.length >= MAX_VISIBLE_TEXTS) break;
235
+ if (!entry || typeof entry !== "object") continue;
236
+ const { key, text } = entry;
237
+ if (typeof key !== "string" || !KEY_PATTERN.test(key) || typeof text !== "string") {
238
+ continue;
239
+ }
240
+ texts.push({ key, text: text.slice(0, MAX_VISIBLE_TEXT_LENGTH) });
241
+ }
242
+ payload = texts;
243
+ }
244
+ const message = {
245
+ source: "livelocale-client",
246
+ type,
247
+ payload
248
+ };
249
+ window.parent.postMessage(message, this.allowedOrigin);
250
+ }
251
+ /**
252
+ * Sets up a listener for messages originating from the parent Studio dashboard.
253
+ */
254
+ listenToParent(callback) {
255
+ if (typeof window === "undefined") {
256
+ return () => {
257
+ };
258
+ }
259
+ const listener = (event) => {
260
+ if (event.origin !== this.allowedOrigin) {
261
+ return;
262
+ }
263
+ if (event.data?.source !== "livelocale-studio") {
264
+ return;
265
+ }
266
+ const { type, payload } = event.data;
267
+ callback(type, payload);
268
+ };
269
+ window.addEventListener("message", listener);
270
+ return () => {
271
+ window.removeEventListener("message", listener);
272
+ };
273
+ }
274
+ };
275
+
150
276
  // src/client/dom-interceptor.ts
151
277
  function debounce(fn, wait) {
152
278
  let timer = null;
@@ -199,6 +325,35 @@ var DOMInterceptor = class {
199
325
  });
200
326
  return Array.from(keys);
201
327
  }
328
+ /**
329
+ * Returns one `{ key, text }` per translation key on the page, in document
330
+ * order, with the text it shows now (watermark removed, trimmed, cut to
331
+ * `MAX_VISIBLE_TEXT_LENGTH`). The first element carrying a key wins; its
332
+ * text is the first descendant text node whose watermark names that key.
333
+ * Elements without such a node, and blank texts, are skipped. At most
334
+ * `limit` entries are returned.
335
+ */
336
+ getVisibleTexts(limit = MAX_VISIBLE_TEXTS) {
337
+ const texts = [];
338
+ const seen = /* @__PURE__ */ new Set();
339
+ const elements = document.querySelectorAll("[data-i18n-key]");
340
+ for (const el of elements) {
341
+ if (texts.length >= limit) break;
342
+ const key = el.getAttribute("data-i18n-key");
343
+ if (!key || seen.has(key)) continue;
344
+ seen.add(key);
345
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
346
+ let node;
347
+ while ((node = walker.nextNode()) !== null) {
348
+ const decoded = decodeKey(node.nodeValue ?? "");
349
+ if (decoded?.key !== key) continue;
350
+ const text = decoded.cleanText.trim().slice(0, MAX_VISIBLE_TEXT_LENGTH);
351
+ if (text) texts.push({ key, text });
352
+ break;
353
+ }
354
+ }
355
+ return texts;
356
+ }
202
357
  /**
203
358
  * Initializes the DOM event interceptors in development mode.
204
359
  * Also launches the watermark scanner and MutationObserver.
@@ -369,7 +524,7 @@ var DOMInterceptor = class {
369
524
  }
370
525
  processTextNode(node) {
371
526
  const raw = node.nodeValue ?? "";
372
- if (!raw.includes("\u200D")) return;
527
+ if (!raw.includes(WATERMARK_DELIMITER)) return;
373
528
  const decoded = decodeKey(raw);
374
529
  if (!decoded) return;
375
530
  const parent = node.parentElement;
@@ -442,10 +597,10 @@ var DOMInterceptor = class {
442
597
  Object.defineProperty(proto, "value", {
443
598
  get() {
444
599
  const raw = original.get.call(this);
445
- return sanitizeZeroWidth(raw);
600
+ return removeWatermarks(raw);
446
601
  },
447
602
  set(val) {
448
- const clean = typeof val === "string" ? sanitizeZeroWidth(val) : val;
603
+ const clean = typeof val === "string" ? removeWatermarks(val) : val;
449
604
  original.set.call(this, clean);
450
605
  },
451
606
  configurable: true
@@ -456,7 +611,7 @@ var DOMInterceptor = class {
456
611
  if (!form) return;
457
612
  const inputs = form.querySelectorAll("input, textarea");
458
613
  inputs.forEach((input) => {
459
- const cleaned = sanitizeZeroWidth(input.value);
614
+ const cleaned = removeWatermarks(input.value);
460
615
  if (cleaned !== input.value) {
461
616
  const proto = input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
462
617
  const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
@@ -547,17 +702,17 @@ var DOMInterceptor = class {
547
702
  };
548
703
  handleCopy = (event) => {
549
704
  const selection = window.getSelection()?.toString() || "";
550
- if (selection.includes("\u200D") || selection.includes("\u200B") || selection.includes("\u200C")) {
551
- const clean = sanitizeZeroWidth(selection);
705
+ if (containsWatermark(selection)) {
706
+ const clean = removeWatermarks(selection);
552
707
  event.clipboardData?.setData("text/plain", clean);
553
708
  event.preventDefault();
554
709
  }
555
710
  };
556
711
  handlePaste = (event) => {
557
712
  const text = event.clipboardData?.getData("text/plain") || "";
558
- if (text.includes("\u200D") || text.includes("\u200B") || text.includes("\u200C")) {
713
+ if (containsWatermark(text)) {
559
714
  event.preventDefault();
560
- const cleaned = sanitizeZeroWidth(text);
715
+ const cleaned = removeWatermarks(text);
561
716
  const targetInput = event.target;
562
717
  if (targetInput && "value" in targetInput) {
563
718
  const proto = targetInput instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
@@ -576,64 +731,6 @@ var DOMInterceptor = class {
576
731
  };
577
732
  };
578
733
 
579
- // src/client/iframe-bridge.ts
580
- var IframeBridge = class {
581
- allowedOrigin;
582
- constructor(allowedOrigin) {
583
- this.allowedOrigin = allowedOrigin;
584
- }
585
- /**
586
- * Sends a message from the iframe client to the parent Studio window.
587
- */
588
- sendToParent(type, payload) {
589
- if (typeof window === "undefined" || !window.parent || window.parent === window) {
590
- return;
591
- }
592
- if (type === "ELEMENT_SELECTED") {
593
- const p = payload;
594
- if (!p || typeof p.key !== "string" || p.key.trim() === "" || !/^[a-zA-Z0-9._-]+$/.test(p.key) || typeof p.currentValue !== "string") {
595
- return;
596
- }
597
- }
598
- if (type === "VISIBLE_KEYS_CHANGED") {
599
- if (!Array.isArray(payload)) return;
600
- const validKeys = payload.filter(
601
- (k) => typeof k === "string" && /^[a-zA-Z0-9._-]+$/.test(k)
602
- );
603
- payload = validKeys;
604
- }
605
- const message = {
606
- source: "livelocale-client",
607
- type,
608
- payload
609
- };
610
- window.parent.postMessage(message, this.allowedOrigin);
611
- }
612
- /**
613
- * Sets up a listener for messages originating from the parent Studio dashboard.
614
- */
615
- listenToParent(callback) {
616
- if (typeof window === "undefined") {
617
- return () => {
618
- };
619
- }
620
- const listener = (event) => {
621
- if (event.origin !== this.allowedOrigin) {
622
- return;
623
- }
624
- if (event.data?.source !== "livelocale-studio") {
625
- return;
626
- }
627
- const { type, payload } = event.data;
628
- callback(type, payload);
629
- };
630
- window.addEventListener("message", listener);
631
- return () => {
632
- window.removeEventListener("message", listener);
633
- };
634
- }
635
- };
636
-
637
734
  // src/client/wrap.ts
638
735
  function extractKey(arg, keyPrefix) {
639
736
  if (typeof arg === "string") {
@@ -770,8 +867,16 @@ function restorePreviews() {
770
867
  previewNodes.clear();
771
868
  }
772
869
  if (typeof window !== "undefined" && isLiveLocaleEnabled() && window.parent !== window) {
773
- const urlParams = new URLSearchParams(window.location.search);
774
- const allowedOrigin = urlParams.get("livelocale-origin") || "http://localhost:3010";
870
+ let storage = null;
871
+ try {
872
+ storage = window.sessionStorage;
873
+ } catch {
874
+ }
875
+ const allowedOrigin = resolveStudioOrigin(
876
+ window.location.search,
877
+ storage,
878
+ readSameOriginParent(window)
879
+ );
775
880
  const bridge = new IframeBridge(allowedOrigin);
776
881
  const interceptor = new DOMInterceptor(
777
882
  (payload) => {
@@ -779,6 +884,7 @@ if (typeof window !== "undefined" && isLiveLocaleEnabled() && window.parent !==
779
884
  },
780
885
  (keys) => {
781
886
  bridge.sendToParent("VISIBLE_KEYS_CHANGED", keys);
887
+ bridge.sendToParent("VISIBLE_TEXTS_CHANGED", interceptor.getVisibleTexts());
782
888
  }
783
889
  );
784
890
  interceptor.init();
@@ -809,6 +915,7 @@ export {
809
915
  decodeKey,
810
916
  encodeKey,
811
917
  hasWatermark,
918
+ removeWatermarks,
812
919
  sanitizeZeroWidth,
813
920
  stripWatermark,
814
921
  unwrap,