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
@@ -154,6 +154,15 @@ function stripWatermark(text) {
154
154
  function sanitizeZeroWidth(text) {
155
155
  return text.replace(ZW_RE, "");
156
156
  }
157
+ function removeWatermarks(text) {
158
+ return text.replace(WATERMARK_RE, "");
159
+ }
160
+ function containsWatermark(text) {
161
+ WATERMARK_RE.lastIndex = 0;
162
+ const found = WATERMARK_RE.test(text);
163
+ WATERMARK_RE.lastIndex = 0;
164
+ return found;
165
+ }
157
166
  function hasWatermark(text) {
158
167
  return text.startsWith(DELIM);
159
168
  }
@@ -169,14 +178,137 @@ function unwrap(value) {
169
178
  }
170
179
  return value;
171
180
  }
172
- var DELIM, BIT_0, BIT_1, ZW_RE;
181
+ var DELIM, BIT_0, BIT_1, WATERMARK_DELIMITER, ZW_RE, WATERMARK_RE;
173
182
  var init_watermark = __esm({
174
183
  "src/client/watermark.ts"() {
175
184
  "use strict";
176
- DELIM = "\u200D";
185
+ DELIM = "\u200C";
177
186
  BIT_0 = "\u200B";
178
- BIT_1 = "\u200C";
187
+ BIT_1 = "\u200D";
188
+ WATERMARK_DELIMITER = DELIM;
179
189
  ZW_RE = /[\u200B\u200C\u200D]/g;
190
+ WATERMARK_RE = /\u200C(?:[\u200B\u200D]{8})+\u200C/g;
191
+ }
192
+ });
193
+
194
+ // src/client/iframe-bridge.ts
195
+ function toHttpOrigin(value) {
196
+ if (!value) return null;
197
+ try {
198
+ const url = new URL(value);
199
+ return url.protocol === "http:" || url.protocol === "https:" ? url.origin : null;
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+ function readSameOriginParent(win) {
205
+ try {
206
+ const parent = win.parent;
207
+ if (!parent || parent === win) return null;
208
+ const own = toHttpOrigin(win.location.origin);
209
+ if (!own) return null;
210
+ return parent.location.origin === own ? own : null;
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+ function resolveStudioOrigin(search, storage, sameOriginParent) {
216
+ const fromParent = toHttpOrigin(sameOriginParent);
217
+ if (fromParent) return fromParent;
218
+ const fromQuery = toHttpOrigin(new URLSearchParams(search).get("livelocale-origin"));
219
+ if (fromQuery) {
220
+ try {
221
+ storage?.setItem(STUDIO_ORIGIN_STORAGE_KEY, fromQuery);
222
+ } catch {
223
+ }
224
+ return fromQuery;
225
+ }
226
+ let remembered = null;
227
+ try {
228
+ remembered = toHttpOrigin(storage?.getItem(STUDIO_ORIGIN_STORAGE_KEY));
229
+ } catch {
230
+ }
231
+ return remembered ?? DEFAULT_STUDIO_ORIGIN;
232
+ }
233
+ var DEFAULT_STUDIO_ORIGIN, STUDIO_ORIGIN_STORAGE_KEY, MAX_VISIBLE_TEXTS, MAX_VISIBLE_TEXT_LENGTH, KEY_PATTERN, IframeBridge;
234
+ var init_iframe_bridge = __esm({
235
+ "src/client/iframe-bridge.ts"() {
236
+ "use strict";
237
+ DEFAULT_STUDIO_ORIGIN = "http://localhost:3010";
238
+ STUDIO_ORIGIN_STORAGE_KEY = "livelocale-studio-origin";
239
+ MAX_VISIBLE_TEXTS = 100;
240
+ MAX_VISIBLE_TEXT_LENGTH = 200;
241
+ KEY_PATTERN = /^[a-zA-Z0-9._-]+$/;
242
+ IframeBridge = class {
243
+ allowedOrigin;
244
+ constructor(allowedOrigin) {
245
+ this.allowedOrigin = allowedOrigin;
246
+ }
247
+ /**
248
+ * Sends a message from the iframe client to the parent Studio window.
249
+ */
250
+ sendToParent(type, payload) {
251
+ if (typeof window === "undefined" || !window.parent || window.parent === window) {
252
+ return;
253
+ }
254
+ if (type === "ELEMENT_SELECTED") {
255
+ const p = payload;
256
+ if (!p || typeof p.key !== "string" || p.key.trim() === "" || !/^[a-zA-Z0-9._-]+$/.test(p.key) || typeof p.currentValue !== "string") {
257
+ return;
258
+ }
259
+ }
260
+ if (type === "VISIBLE_KEYS_CHANGED") {
261
+ if (!Array.isArray(payload)) return;
262
+ const validKeys = payload.filter(
263
+ (k) => typeof k === "string" && /^[a-zA-Z0-9._-]+$/.test(k)
264
+ );
265
+ payload = validKeys;
266
+ }
267
+ if (type === "VISIBLE_TEXTS_CHANGED") {
268
+ if (!Array.isArray(payload)) return;
269
+ const texts = [];
270
+ for (const entry of payload) {
271
+ if (texts.length >= MAX_VISIBLE_TEXTS) break;
272
+ if (!entry || typeof entry !== "object") continue;
273
+ const { key, text } = entry;
274
+ if (typeof key !== "string" || !KEY_PATTERN.test(key) || typeof text !== "string") {
275
+ continue;
276
+ }
277
+ texts.push({ key, text: text.slice(0, MAX_VISIBLE_TEXT_LENGTH) });
278
+ }
279
+ payload = texts;
280
+ }
281
+ const message = {
282
+ source: "livelocale-client",
283
+ type,
284
+ payload
285
+ };
286
+ window.parent.postMessage(message, this.allowedOrigin);
287
+ }
288
+ /**
289
+ * Sets up a listener for messages originating from the parent Studio dashboard.
290
+ */
291
+ listenToParent(callback) {
292
+ if (typeof window === "undefined") {
293
+ return () => {
294
+ };
295
+ }
296
+ const listener = (event) => {
297
+ if (event.origin !== this.allowedOrigin) {
298
+ return;
299
+ }
300
+ if (event.data?.source !== "livelocale-studio") {
301
+ return;
302
+ }
303
+ const { type, payload } = event.data;
304
+ callback(type, payload);
305
+ };
306
+ window.addEventListener("message", listener);
307
+ return () => {
308
+ window.removeEventListener("message", listener);
309
+ };
310
+ }
311
+ };
180
312
  }
181
313
  });
182
314
 
@@ -204,6 +336,7 @@ var init_dom_interceptor = __esm({
204
336
  "use strict";
205
337
  init_watermark();
206
338
  init_env();
339
+ init_iframe_bridge();
207
340
  DOMInterceptor = class {
208
341
  onElementSelected;
209
342
  onVisibleKeysChanged;
@@ -238,6 +371,35 @@ var init_dom_interceptor = __esm({
238
371
  });
239
372
  return Array.from(keys);
240
373
  }
374
+ /**
375
+ * Returns one `{ key, text }` per translation key on the page, in document
376
+ * order, with the text it shows now (watermark removed, trimmed, cut to
377
+ * `MAX_VISIBLE_TEXT_LENGTH`). The first element carrying a key wins; its
378
+ * text is the first descendant text node whose watermark names that key.
379
+ * Elements without such a node, and blank texts, are skipped. At most
380
+ * `limit` entries are returned.
381
+ */
382
+ getVisibleTexts(limit = MAX_VISIBLE_TEXTS) {
383
+ const texts = [];
384
+ const seen = /* @__PURE__ */ new Set();
385
+ const elements = document.querySelectorAll("[data-i18n-key]");
386
+ for (const el of elements) {
387
+ if (texts.length >= limit) break;
388
+ const key = el.getAttribute("data-i18n-key");
389
+ if (!key || seen.has(key)) continue;
390
+ seen.add(key);
391
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
392
+ let node;
393
+ while ((node = walker.nextNode()) !== null) {
394
+ const decoded = decodeKey(node.nodeValue ?? "");
395
+ if (decoded?.key !== key) continue;
396
+ const text = decoded.cleanText.trim().slice(0, MAX_VISIBLE_TEXT_LENGTH);
397
+ if (text) texts.push({ key, text });
398
+ break;
399
+ }
400
+ }
401
+ return texts;
402
+ }
241
403
  /**
242
404
  * Initializes the DOM event interceptors in development mode.
243
405
  * Also launches the watermark scanner and MutationObserver.
@@ -408,7 +570,7 @@ var init_dom_interceptor = __esm({
408
570
  }
409
571
  processTextNode(node) {
410
572
  const raw = node.nodeValue ?? "";
411
- if (!raw.includes("\u200D")) return;
573
+ if (!raw.includes(WATERMARK_DELIMITER)) return;
412
574
  const decoded = decodeKey(raw);
413
575
  if (!decoded) return;
414
576
  const parent = node.parentElement;
@@ -481,10 +643,10 @@ var init_dom_interceptor = __esm({
481
643
  Object.defineProperty(proto, "value", {
482
644
  get() {
483
645
  const raw = original.get.call(this);
484
- return sanitizeZeroWidth(raw);
646
+ return removeWatermarks(raw);
485
647
  },
486
648
  set(val) {
487
- const clean = typeof val === "string" ? sanitizeZeroWidth(val) : val;
649
+ const clean = typeof val === "string" ? removeWatermarks(val) : val;
488
650
  original.set.call(this, clean);
489
651
  },
490
652
  configurable: true
@@ -495,7 +657,7 @@ var init_dom_interceptor = __esm({
495
657
  if (!form) return;
496
658
  const inputs = form.querySelectorAll("input, textarea");
497
659
  inputs.forEach((input) => {
498
- const cleaned = sanitizeZeroWidth(input.value);
660
+ const cleaned = removeWatermarks(input.value);
499
661
  if (cleaned !== input.value) {
500
662
  const proto = input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
501
663
  const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
@@ -586,17 +748,17 @@ var init_dom_interceptor = __esm({
586
748
  };
587
749
  handleCopy = (event) => {
588
750
  const selection = window.getSelection()?.toString() || "";
589
- if (selection.includes("\u200D") || selection.includes("\u200B") || selection.includes("\u200C")) {
590
- const clean = sanitizeZeroWidth(selection);
751
+ if (containsWatermark(selection)) {
752
+ const clean = removeWatermarks(selection);
591
753
  event.clipboardData?.setData("text/plain", clean);
592
754
  event.preventDefault();
593
755
  }
594
756
  };
595
757
  handlePaste = (event) => {
596
758
  const text = event.clipboardData?.getData("text/plain") || "";
597
- if (text.includes("\u200D") || text.includes("\u200B") || text.includes("\u200C")) {
759
+ if (containsWatermark(text)) {
598
760
  event.preventDefault();
599
- const cleaned = sanitizeZeroWidth(text);
761
+ const cleaned = removeWatermarks(text);
600
762
  const targetInput = event.target;
601
763
  if (targetInput && "value" in targetInput) {
602
764
  const proto = targetInput instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
@@ -617,70 +779,6 @@ var init_dom_interceptor = __esm({
617
779
  }
618
780
  });
619
781
 
620
- // src/client/iframe-bridge.ts
621
- var IframeBridge;
622
- var init_iframe_bridge = __esm({
623
- "src/client/iframe-bridge.ts"() {
624
- "use strict";
625
- IframeBridge = class {
626
- allowedOrigin;
627
- constructor(allowedOrigin) {
628
- this.allowedOrigin = allowedOrigin;
629
- }
630
- /**
631
- * Sends a message from the iframe client to the parent Studio window.
632
- */
633
- sendToParent(type, payload) {
634
- if (typeof window === "undefined" || !window.parent || window.parent === window) {
635
- return;
636
- }
637
- if (type === "ELEMENT_SELECTED") {
638
- const p = payload;
639
- if (!p || typeof p.key !== "string" || p.key.trim() === "" || !/^[a-zA-Z0-9._-]+$/.test(p.key) || typeof p.currentValue !== "string") {
640
- return;
641
- }
642
- }
643
- if (type === "VISIBLE_KEYS_CHANGED") {
644
- if (!Array.isArray(payload)) return;
645
- const validKeys = payload.filter(
646
- (k) => typeof k === "string" && /^[a-zA-Z0-9._-]+$/.test(k)
647
- );
648
- payload = validKeys;
649
- }
650
- const message = {
651
- source: "livelocale-client",
652
- type,
653
- payload
654
- };
655
- window.parent.postMessage(message, this.allowedOrigin);
656
- }
657
- /**
658
- * Sets up a listener for messages originating from the parent Studio dashboard.
659
- */
660
- listenToParent(callback) {
661
- if (typeof window === "undefined") {
662
- return () => {
663
- };
664
- }
665
- const listener = (event) => {
666
- if (event.origin !== this.allowedOrigin) {
667
- return;
668
- }
669
- if (event.data?.source !== "livelocale-studio") {
670
- return;
671
- }
672
- const { type, payload } = event.data;
673
- callback(type, payload);
674
- };
675
- window.addEventListener("message", listener);
676
- return () => {
677
- window.removeEventListener("message", listener);
678
- };
679
- }
680
- };
681
- }
682
- });
683
-
684
782
  // src/client/wrap.ts
685
783
  function extractKey(arg, keyPrefix) {
686
784
  if (typeof arg === "string") {
@@ -799,6 +897,7 @@ __export(client_exports, {
799
897
  decodeKey: () => decodeKey,
800
898
  encodeKey: () => encodeKey,
801
899
  hasWatermark: () => hasWatermark,
900
+ removeWatermarks: () => removeWatermarks,
802
901
  sanitizeZeroWidth: () => sanitizeZeroWidth,
803
902
  stripWatermark: () => stripWatermark,
804
903
  unwrap: () => unwrap,
@@ -850,8 +949,16 @@ var init_client = __esm({
850
949
  init_wrap();
851
950
  previewNodes = /* @__PURE__ */ new Map();
852
951
  if (typeof window !== "undefined" && isLiveLocaleEnabled() && window.parent !== window) {
853
- const urlParams = new URLSearchParams(window.location.search);
854
- const allowedOrigin = urlParams.get("livelocale-origin") || "http://localhost:3010";
952
+ let storage = null;
953
+ try {
954
+ storage = window.sessionStorage;
955
+ } catch {
956
+ }
957
+ const allowedOrigin = resolveStudioOrigin(
958
+ window.location.search,
959
+ storage,
960
+ readSameOriginParent(window)
961
+ );
855
962
  const bridge = new IframeBridge(allowedOrigin);
856
963
  const interceptor = new DOMInterceptor(
857
964
  (payload) => {
@@ -859,6 +966,7 @@ var init_client = __esm({
859
966
  },
860
967
  (keys) => {
861
968
  bridge.sendToParent("VISIBLE_KEYS_CHANGED", keys);
969
+ bridge.sendToParent("VISIBLE_TEXTS_CHANGED", interceptor.getVisibleTexts());
862
970
  }
863
971
  );
864
972
  interceptor.init();