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
@@ -142,6 +142,15 @@ function stripWatermark(text) {
142
142
  function sanitizeZeroWidth(text) {
143
143
  return text.replace(ZW_RE, "");
144
144
  }
145
+ function removeWatermarks(text) {
146
+ return text.replace(WATERMARK_RE, "");
147
+ }
148
+ function containsWatermark(text) {
149
+ WATERMARK_RE.lastIndex = 0;
150
+ const found = WATERMARK_RE.test(text);
151
+ WATERMARK_RE.lastIndex = 0;
152
+ return found;
153
+ }
145
154
  function hasWatermark(text) {
146
155
  return text.startsWith(DELIM);
147
156
  }
@@ -157,14 +166,137 @@ function unwrap(value) {
157
166
  }
158
167
  return value;
159
168
  }
160
- var DELIM, BIT_0, BIT_1, ZW_RE;
169
+ var DELIM, BIT_0, BIT_1, WATERMARK_DELIMITER, ZW_RE, WATERMARK_RE;
161
170
  var init_watermark = __esm({
162
171
  "src/client/watermark.ts"() {
163
172
  "use strict";
164
- DELIM = "\u200D";
173
+ DELIM = "\u200C";
165
174
  BIT_0 = "\u200B";
166
- BIT_1 = "\u200C";
175
+ BIT_1 = "\u200D";
176
+ WATERMARK_DELIMITER = DELIM;
167
177
  ZW_RE = /[\u200B\u200C\u200D]/g;
178
+ WATERMARK_RE = /\u200C(?:[\u200B\u200D]{8})+\u200C/g;
179
+ }
180
+ });
181
+
182
+ // src/client/iframe-bridge.ts
183
+ function toHttpOrigin(value) {
184
+ if (!value) return null;
185
+ try {
186
+ const url = new URL(value);
187
+ return url.protocol === "http:" || url.protocol === "https:" ? url.origin : null;
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+ function readSameOriginParent(win) {
193
+ try {
194
+ const parent = win.parent;
195
+ if (!parent || parent === win) return null;
196
+ const own = toHttpOrigin(win.location.origin);
197
+ if (!own) return null;
198
+ return parent.location.origin === own ? own : null;
199
+ } catch {
200
+ return null;
201
+ }
202
+ }
203
+ function resolveStudioOrigin(search, storage, sameOriginParent) {
204
+ const fromParent = toHttpOrigin(sameOriginParent);
205
+ if (fromParent) return fromParent;
206
+ const fromQuery = toHttpOrigin(new URLSearchParams(search).get("livelocale-origin"));
207
+ if (fromQuery) {
208
+ try {
209
+ storage?.setItem(STUDIO_ORIGIN_STORAGE_KEY, fromQuery);
210
+ } catch {
211
+ }
212
+ return fromQuery;
213
+ }
214
+ let remembered = null;
215
+ try {
216
+ remembered = toHttpOrigin(storage?.getItem(STUDIO_ORIGIN_STORAGE_KEY));
217
+ } catch {
218
+ }
219
+ return remembered ?? DEFAULT_STUDIO_ORIGIN;
220
+ }
221
+ var DEFAULT_STUDIO_ORIGIN, STUDIO_ORIGIN_STORAGE_KEY, MAX_VISIBLE_TEXTS, MAX_VISIBLE_TEXT_LENGTH, KEY_PATTERN, IframeBridge;
222
+ var init_iframe_bridge = __esm({
223
+ "src/client/iframe-bridge.ts"() {
224
+ "use strict";
225
+ DEFAULT_STUDIO_ORIGIN = "http://localhost:3010";
226
+ STUDIO_ORIGIN_STORAGE_KEY = "livelocale-studio-origin";
227
+ MAX_VISIBLE_TEXTS = 100;
228
+ MAX_VISIBLE_TEXT_LENGTH = 200;
229
+ KEY_PATTERN = /^[a-zA-Z0-9._-]+$/;
230
+ IframeBridge = class {
231
+ allowedOrigin;
232
+ constructor(allowedOrigin) {
233
+ this.allowedOrigin = allowedOrigin;
234
+ }
235
+ /**
236
+ * Sends a message from the iframe client to the parent Studio window.
237
+ */
238
+ sendToParent(type, payload) {
239
+ if (typeof window === "undefined" || !window.parent || window.parent === window) {
240
+ return;
241
+ }
242
+ if (type === "ELEMENT_SELECTED") {
243
+ const p = payload;
244
+ if (!p || typeof p.key !== "string" || p.key.trim() === "" || !/^[a-zA-Z0-9._-]+$/.test(p.key) || typeof p.currentValue !== "string") {
245
+ return;
246
+ }
247
+ }
248
+ if (type === "VISIBLE_KEYS_CHANGED") {
249
+ if (!Array.isArray(payload)) return;
250
+ const validKeys = payload.filter(
251
+ (k) => typeof k === "string" && /^[a-zA-Z0-9._-]+$/.test(k)
252
+ );
253
+ payload = validKeys;
254
+ }
255
+ if (type === "VISIBLE_TEXTS_CHANGED") {
256
+ if (!Array.isArray(payload)) return;
257
+ const texts = [];
258
+ for (const entry of payload) {
259
+ if (texts.length >= MAX_VISIBLE_TEXTS) break;
260
+ if (!entry || typeof entry !== "object") continue;
261
+ const { key, text } = entry;
262
+ if (typeof key !== "string" || !KEY_PATTERN.test(key) || typeof text !== "string") {
263
+ continue;
264
+ }
265
+ texts.push({ key, text: text.slice(0, MAX_VISIBLE_TEXT_LENGTH) });
266
+ }
267
+ payload = texts;
268
+ }
269
+ const message = {
270
+ source: "livelocale-client",
271
+ type,
272
+ payload
273
+ };
274
+ window.parent.postMessage(message, this.allowedOrigin);
275
+ }
276
+ /**
277
+ * Sets up a listener for messages originating from the parent Studio dashboard.
278
+ */
279
+ listenToParent(callback) {
280
+ if (typeof window === "undefined") {
281
+ return () => {
282
+ };
283
+ }
284
+ const listener = (event) => {
285
+ if (event.origin !== this.allowedOrigin) {
286
+ return;
287
+ }
288
+ if (event.data?.source !== "livelocale-studio") {
289
+ return;
290
+ }
291
+ const { type, payload } = event.data;
292
+ callback(type, payload);
293
+ };
294
+ window.addEventListener("message", listener);
295
+ return () => {
296
+ window.removeEventListener("message", listener);
297
+ };
298
+ }
299
+ };
168
300
  }
169
301
  });
170
302
 
@@ -192,6 +324,7 @@ var init_dom_interceptor = __esm({
192
324
  "use strict";
193
325
  init_watermark();
194
326
  init_env();
327
+ init_iframe_bridge();
195
328
  DOMInterceptor = class {
196
329
  onElementSelected;
197
330
  onVisibleKeysChanged;
@@ -226,6 +359,35 @@ var init_dom_interceptor = __esm({
226
359
  });
227
360
  return Array.from(keys);
228
361
  }
362
+ /**
363
+ * Returns one `{ key, text }` per translation key on the page, in document
364
+ * order, with the text it shows now (watermark removed, trimmed, cut to
365
+ * `MAX_VISIBLE_TEXT_LENGTH`). The first element carrying a key wins; its
366
+ * text is the first descendant text node whose watermark names that key.
367
+ * Elements without such a node, and blank texts, are skipped. At most
368
+ * `limit` entries are returned.
369
+ */
370
+ getVisibleTexts(limit = MAX_VISIBLE_TEXTS) {
371
+ const texts = [];
372
+ const seen = /* @__PURE__ */ new Set();
373
+ const elements = document.querySelectorAll("[data-i18n-key]");
374
+ for (const el of elements) {
375
+ if (texts.length >= limit) break;
376
+ const key = el.getAttribute("data-i18n-key");
377
+ if (!key || seen.has(key)) continue;
378
+ seen.add(key);
379
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
380
+ let node;
381
+ while ((node = walker.nextNode()) !== null) {
382
+ const decoded = decodeKey(node.nodeValue ?? "");
383
+ if (decoded?.key !== key) continue;
384
+ const text = decoded.cleanText.trim().slice(0, MAX_VISIBLE_TEXT_LENGTH);
385
+ if (text) texts.push({ key, text });
386
+ break;
387
+ }
388
+ }
389
+ return texts;
390
+ }
229
391
  /**
230
392
  * Initializes the DOM event interceptors in development mode.
231
393
  * Also launches the watermark scanner and MutationObserver.
@@ -396,7 +558,7 @@ var init_dom_interceptor = __esm({
396
558
  }
397
559
  processTextNode(node) {
398
560
  const raw = node.nodeValue ?? "";
399
- if (!raw.includes("\u200D")) return;
561
+ if (!raw.includes(WATERMARK_DELIMITER)) return;
400
562
  const decoded = decodeKey(raw);
401
563
  if (!decoded) return;
402
564
  const parent = node.parentElement;
@@ -469,10 +631,10 @@ var init_dom_interceptor = __esm({
469
631
  Object.defineProperty(proto, "value", {
470
632
  get() {
471
633
  const raw = original.get.call(this);
472
- return sanitizeZeroWidth(raw);
634
+ return removeWatermarks(raw);
473
635
  },
474
636
  set(val) {
475
- const clean = typeof val === "string" ? sanitizeZeroWidth(val) : val;
637
+ const clean = typeof val === "string" ? removeWatermarks(val) : val;
476
638
  original.set.call(this, clean);
477
639
  },
478
640
  configurable: true
@@ -483,7 +645,7 @@ var init_dom_interceptor = __esm({
483
645
  if (!form) return;
484
646
  const inputs = form.querySelectorAll("input, textarea");
485
647
  inputs.forEach((input) => {
486
- const cleaned = sanitizeZeroWidth(input.value);
648
+ const cleaned = removeWatermarks(input.value);
487
649
  if (cleaned !== input.value) {
488
650
  const proto = input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
489
651
  const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
@@ -574,17 +736,17 @@ var init_dom_interceptor = __esm({
574
736
  };
575
737
  handleCopy = (event) => {
576
738
  const selection = window.getSelection()?.toString() || "";
577
- if (selection.includes("\u200D") || selection.includes("\u200B") || selection.includes("\u200C")) {
578
- const clean = sanitizeZeroWidth(selection);
739
+ if (containsWatermark(selection)) {
740
+ const clean = removeWatermarks(selection);
579
741
  event.clipboardData?.setData("text/plain", clean);
580
742
  event.preventDefault();
581
743
  }
582
744
  };
583
745
  handlePaste = (event) => {
584
746
  const text = event.clipboardData?.getData("text/plain") || "";
585
- if (text.includes("\u200D") || text.includes("\u200B") || text.includes("\u200C")) {
747
+ if (containsWatermark(text)) {
586
748
  event.preventDefault();
587
- const cleaned = sanitizeZeroWidth(text);
749
+ const cleaned = removeWatermarks(text);
588
750
  const targetInput = event.target;
589
751
  if (targetInput && "value" in targetInput) {
590
752
  const proto = targetInput instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
@@ -605,70 +767,6 @@ var init_dom_interceptor = __esm({
605
767
  }
606
768
  });
607
769
 
608
- // src/client/iframe-bridge.ts
609
- var IframeBridge;
610
- var init_iframe_bridge = __esm({
611
- "src/client/iframe-bridge.ts"() {
612
- "use strict";
613
- IframeBridge = class {
614
- allowedOrigin;
615
- constructor(allowedOrigin) {
616
- this.allowedOrigin = allowedOrigin;
617
- }
618
- /**
619
- * Sends a message from the iframe client to the parent Studio window.
620
- */
621
- sendToParent(type, payload) {
622
- if (typeof window === "undefined" || !window.parent || window.parent === window) {
623
- return;
624
- }
625
- if (type === "ELEMENT_SELECTED") {
626
- const p = payload;
627
- if (!p || typeof p.key !== "string" || p.key.trim() === "" || !/^[a-zA-Z0-9._-]+$/.test(p.key) || typeof p.currentValue !== "string") {
628
- return;
629
- }
630
- }
631
- if (type === "VISIBLE_KEYS_CHANGED") {
632
- if (!Array.isArray(payload)) return;
633
- const validKeys = payload.filter(
634
- (k) => typeof k === "string" && /^[a-zA-Z0-9._-]+$/.test(k)
635
- );
636
- payload = validKeys;
637
- }
638
- const message = {
639
- source: "livelocale-client",
640
- type,
641
- payload
642
- };
643
- window.parent.postMessage(message, this.allowedOrigin);
644
- }
645
- /**
646
- * Sets up a listener for messages originating from the parent Studio dashboard.
647
- */
648
- listenToParent(callback) {
649
- if (typeof window === "undefined") {
650
- return () => {
651
- };
652
- }
653
- const listener = (event) => {
654
- if (event.origin !== this.allowedOrigin) {
655
- return;
656
- }
657
- if (event.data?.source !== "livelocale-studio") {
658
- return;
659
- }
660
- const { type, payload } = event.data;
661
- callback(type, payload);
662
- };
663
- window.addEventListener("message", listener);
664
- return () => {
665
- window.removeEventListener("message", listener);
666
- };
667
- }
668
- };
669
- }
670
- });
671
-
672
770
  // src/client/wrap.ts
673
771
  function extractKey(arg, keyPrefix) {
674
772
  if (typeof arg === "string") {
@@ -787,6 +885,7 @@ __export(client_exports, {
787
885
  decodeKey: () => decodeKey,
788
886
  encodeKey: () => encodeKey,
789
887
  hasWatermark: () => hasWatermark,
888
+ removeWatermarks: () => removeWatermarks,
790
889
  sanitizeZeroWidth: () => sanitizeZeroWidth,
791
890
  stripWatermark: () => stripWatermark,
792
891
  unwrap: () => unwrap,
@@ -838,8 +937,16 @@ var init_client = __esm({
838
937
  init_wrap();
839
938
  previewNodes = /* @__PURE__ */ new Map();
840
939
  if (typeof window !== "undefined" && isLiveLocaleEnabled() && window.parent !== window) {
841
- const urlParams = new URLSearchParams(window.location.search);
842
- const allowedOrigin = urlParams.get("livelocale-origin") || "http://localhost:3010";
940
+ let storage = null;
941
+ try {
942
+ storage = window.sessionStorage;
943
+ } catch {
944
+ }
945
+ const allowedOrigin = resolveStudioOrigin(
946
+ window.location.search,
947
+ storage,
948
+ readSameOriginParent(window)
949
+ );
843
950
  const bridge = new IframeBridge(allowedOrigin);
844
951
  const interceptor = new DOMInterceptor(
845
952
  (payload) => {
@@ -847,6 +954,7 @@ var init_client = __esm({
847
954
  },
848
955
  (keys) => {
849
956
  bridge.sendToParent("VISIBLE_KEYS_CHANGED", keys);
957
+ bridge.sendToParent("VISIBLE_TEXTS_CHANGED", interceptor.getVisibleTexts());
850
958
  }
851
959
  );
852
960
  interceptor.init();