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
package/README.md CHANGED
@@ -212,11 +212,21 @@ Inspects the project for the integration problems the Studio cannot recover from
212
212
  probes the running app.
213
213
 
214
214
  ```bash
215
- npx livelocale doctor [--dir <path>] [--locales <path>] [--app <origin>] [--token <token>]
215
+ npx livelocale doctor [--dir <path>] [--locales <path>] [--app <url>] [--share] [--token <token>]
216
216
  ```
217
217
 
218
218
  - `--locales` — locales directory relative to the project (default `locales`).
219
- - `--app` — also check that the mutation API answers and that pages are embeddable.
219
+ - `--app` — also check that the mutation API answers and that pages are embeddable. A path is
220
+ the app base: `--app https://user.github.io/repo/` probes a GitHub Pages project site (a
221
+ trailing `index.html` is dropped).
222
+ - `--share` — run the share link probes even when `<base>livelocale/manifest.json` does not
223
+ answer. They also run on their own whenever the manifest answers with JSON. They check the
224
+ Studio page, the manifest, `flag.js` and its tag first in `<head>`, frame headers
225
+ (`X-Frame-Options: DENY` or `frame-ancestors 'none'` breaks the Studio; `SAMEORIGIN` is fine),
226
+ `script-src`, and service workers, then print `🔗 Share link: <base>livelocale/index.html`.
227
+ When a Workbox service worker (vite-plugin-pwa, workbox-webpack-plugin) would answer the Studio
228
+ with your app shell, add this line to its Workbox options:
229
+ `navigateFallbackDenylist: [/^\/livelocale\//]`.
220
230
  - `--token` — access token for a preview deployment (defaults to `LIVELOCALE_TOKEN`); with it the
221
231
  doctor also reports the server mode and repository access problems.
222
232
 
@@ -254,6 +264,85 @@ English's two are not reported.
254
264
  - run: npx livelocale check
255
265
  ```
256
266
 
267
+ ### `pack`
268
+
269
+ Turns a finished static build into a share link: it writes the Studio, `manifest.json`, the locale
270
+ files and `flag.js` into `<outDir>/livelocale/` and links `flag.js` from every HTML page. Run it
271
+ after your build, for example `"build": "vite build && livelocale pack dist"`.
272
+
273
+ ```bash
274
+ npx livelocale pack <outDir> [--dir <path>] [--locales <path>] [--source <locale>] [--base <path>] [--expose-repo] [--no-inject]
275
+ ```
276
+
277
+ - `<outDir>` — the built site folder, relative to `--dir` (for example `dist`, `build` or `out`).
278
+ - `--locales` — locales directory relative to the project (default `locales`).
279
+ - `--source` — source locale (default `en`, else the first locale).
280
+ - `--base` — site path the app is served from, such as `/repo/` (default: detected from the
281
+ project, else `/`). A relative base (`./`) prints a warning; pass an absolute one if the app has
282
+ client-side routes.
283
+ - `--expose-repo` — put the GitHub `owner/repo` into `livelocale/manifest.json` (off by default).
284
+ - `--no-inject` — do not add the `flag.js` script to HTML files. Next.js outputs are never injected.
285
+
286
+ The share link is `<your site><base>livelocale/index.html`. Nothing is written outside
287
+ `<outDir>/livelocale/` except that one script tag, and an existing `livelocale/` folder is replaced
288
+ only when pack wrote it.
289
+
290
+ ### `build`
291
+
292
+ Runs your build command and packs its output in one step. The environment variables work the same
293
+ on Windows, macOS and Linux, so no `cross-env` is needed.
294
+
295
+ ```bash
296
+ npx livelocale build [--preview] [--out <dir>] [pack options] -- <your build command>
297
+ ```
298
+
299
+ - `--preview` — run the command with `LIVELOCALE=preview` and `NEXT_PUBLIC_LIVELOCALE=preview`, so
300
+ server-rendered and pre-rendered pages carry watermarks.
301
+ - `--out` — the build output folder (default: the first of `dist/`, `build/` or `out/` that holds an
302
+ `index.html` written by this build). Next.js projects always pack into `public/livelocale/`
303
+ before the build runs, so Next copies it into the output.
304
+ - `--dir`, `--locales`, `--source`, `--base` and `--expose-repo` work as they do for `pack`.
305
+
306
+ | Stack | Build change |
307
+ | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
308
+ | Next.js `output: 'export'` | `"build:preview": "livelocale build --preview -- next build"`, deployed to a staging location, because the HTML is pre-rendered and must be watermarked at build time. |
309
+ | Next.js on Vercel/Node | For the Preview environment, use the build command `livelocale build --preview -- next build`. It packs into a gitignored `public/livelocale/`. |
310
+ | Custom CI | `LIVELOCALE=preview <your build>` (only for SSR/SSG), then `npx livelocale pack <outDir>`. |
311
+
312
+ A static export stops before building when `app/api/livelocale/route.*` exports POST, because export
313
+ mode only allows GET route handlers; the share link does not need that route.
314
+
315
+ ### `apply`
316
+
317
+ Applies a LiveLocale change link (the `lc1.` link a translator sends from the share link) to your
318
+ locale files. Every edit is checked again against your own files and source locale before anything
319
+ is written.
320
+
321
+ ```bash
322
+ npx livelocale apply "<link>" [--pr] [--dir <path>] [--locales <path>] [--source <locale>] [--force] [--dry-run]
323
+ npx livelocale apply changes.txt
324
+ npx livelocale apply - < changes.txt
325
+ npx livelocale apply --clipboard
326
+ ```
327
+
328
+ - The input is the change link itself (or the Studio link that carries it), a file that holds it
329
+ (the change file Send offers, or a saved email or issue), `-` to read standard input, or
330
+ `--clipboard` to read the clipboard. Pass the link or `--clipboard`, not both.
331
+ - cmd.exe cuts command lines at 8,191 characters, so on Windows pass a long link as a file or with
332
+ `--clipboard`.
333
+ - `--pr` — commit the changes on a new `livelocale/apply-<time>` branch, push it and open a pull
334
+ request.
335
+ - `--locales` and `--source` work as they do for `check` (default `locales`, and `en`, else the
336
+ first locale).
337
+ - `--dry-run` — plan the edits and print the summary without writing any file.
338
+ - `--force` — also apply edits the translator saved with "Save anyway" (for example a dropped
339
+ placeholder). Without it they are reported as `needs --force` and skipped.
340
+
341
+ The summary lists each edit's locale, key and status (applied, already done, conflict, needs
342
+ --force or rejected) and never prints translation values. The exit code is 0 when every edit was
343
+ applied or already done, and 1 when any edit is a conflict, needs `--force` or was rejected, or
344
+ when the link cannot be read.
345
+
257
346
  ### `migrate`
258
347
 
259
348
  Performs static analysis to scan and automatically wrap existing `react-i18next` hooks with `wrapTranslationEngine`.
@@ -286,6 +375,33 @@ export default defineConfig({
286
375
  });
287
376
  ```
288
377
 
378
+ ### Share link from `vite build`
379
+
380
+ Set `share: true` and `vite build` also writes a share link: the Studio, `manifest.json`, the
381
+ locale files and `flag.js` go into `<outDir>/livelocale/`, and `index.html` loads
382
+ `<base>livelocale/flag.js` first in `<head>`. Deploy the output anywhere static and send
383
+ `<origin><base>livelocale/index.html`. Without `share` the build output is unchanged.
384
+
385
+ ```typescript
386
+ liveLocaleVite({
387
+ localesPath: './locales',
388
+ share: true, // write <outDir>/livelocale/ on vite build (default: false)
389
+ exposeRepo: false, // put the GitHub owner/repo into the manifest (default: false)
390
+ sourceLocale: 'en', // default: en when present, else the first locale
391
+ shareBase: '/app/', // the site path the HTML is served from (default: Vite base)
392
+ });
393
+ ```
394
+
395
+ | Option | Default | What it does |
396
+ | -------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
397
+ | `share` | `false` | Write the share Studio into `<outDir>/livelocale/` on `vite build`. SSR and library builds are skipped. |
398
+ | `exposeRepo` | `false` | Put the GitHub `owner/repo` into the published manifest and the change link. Leave it off for a private repository. |
399
+ | `sourceLocale` | `en` when present, else the first locale | The locale placeholders are checked against. |
400
+ | `shareBase` | Vite `base` | The path the HTML is served from. Set it when `base` is a full URL (a CDN), which the build otherwise rejects with `BAD_BASE`, or differs. |
401
+
402
+ A relative `base` (`./`) links `flag.js` per HTML file and prints a warning: pages served by a
403
+ client-side route fallback cannot load it, so prefer an absolute `base` for apps with such routes.
404
+
289
405
  ---
290
406
 
291
407
  ## 🧾 Reviewing and committing from the Studio
@@ -346,6 +462,115 @@ value when you edit the source language itself.
346
462
  For API clients: send `reference` with the POST to enable the check, and `force: true` to bypass
347
463
  it. Without `reference` only the syntax check runs.
348
464
 
465
+ ## 🔗 Share link on any host
466
+
467
+ A share link lets a translator fix copy on your deployed site, with no server, no account and no
468
+ credential anywhere. The Studio ships inside your build at `<base>livelocale/`, so any host that
469
+ serves the site serves the Studio too: Apache, Nginx, S3, GitHub Pages, Netlify, Cloudflare Pages
470
+ or Vercel. The translator's fixes stay in their own browser until they send you a change link, and
471
+ you turn that link into a pull request with one command.
472
+
473
+ ### Setup
474
+
475
+ | Stack | Build change |
476
+ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
477
+ | Vite | `share: true` in `liveLocaleVite(...)` (see [Share link from `vite build`](#share-link-from-vite-build)). |
478
+ | CRA, webpack and Rsbuild | `"build": "<bundler> && livelocale pack build"`, for example `"build": "react-scripts build && livelocale pack build"`. |
479
+ | Next.js `output: 'export'` | `livelocale build --preview -- next build`, deployed to a staging location, because the HTML is pre-rendered and must be watermarked. |
480
+ | Next.js on Vercel/Node | `livelocale build --preview -- next build` as the Preview environment's build command. It packs into a gitignored `public/livelocale/`. |
481
+ | Custom CI | `LIVELOCALE=preview <build>` (only for SSR/SSG), then `npx livelocale pack <outDir>`. |
482
+
483
+ For CRA, webpack and Rsbuild, use an explicit `&&`, not a `postbuild` script: Yarn Berry and some
484
+ pnpm versions skip pre/post scripts, and hosts that call the bundler directly skip them too.
485
+
486
+ Deploy the output as usual, then check it and print the share link:
487
+
488
+ ```bash
489
+ npx livelocale doctor --app <url>
490
+ ```
491
+
492
+ The doctor checks the Studio page, the manifest, `flag.js`, frame headers and service workers, then
493
+ prints `🔗 Share link: <base>livelocale/index.html`. Pin that link for your translators.
494
+
495
+ ### Translator steps
496
+
497
+ 1. Open the share link in a desktop browser. There is no sign-up, no password and nothing to
498
+ install. If the site has its own login, sign in as usual.
499
+ 2. On the first visit, read the three tips and, optionally, type your name so you are credited in
500
+ the pull request.
501
+ 3. Check the language shown at the top. It is detected from the page text; change it if it is
502
+ wrong.
503
+ 4. Hold **Alt** (Option on Mac) and click the wrong text. The editor shows the source text, the
504
+ current translation, the placeholders that must stay, and one field per plural form.
505
+ 5. Type the fix and press **Save** (Ctrl+Enter). When a placeholder is missing, a message explains
506
+ it, and **Save anyway** stays available; it marks the edit for the developer.
507
+ 6. Keep browsing and fixing. Changes survive reloads and closing the tab. A counter shows how many
508
+ are not sent yet ("5 changes not sent"), and **Changes** lists each one with Undo.
509
+ 7. Click **Send to developer** and add an optional note. Then choose **Copy link** and paste it in
510
+ any chat or email, or **Download file** for a large batch.
511
+ 8. Sent changes stay in the list, marked "Sent".
512
+
513
+ ### Developer
514
+
515
+ Open the change link. The Studio opens in **Review mode**, with the proposed edits shown read-only
516
+ on the real page and a before/after table with the check results. Then apply it:
517
+
518
+ ```bash
519
+ npx livelocale apply "<link or file>" --pr
520
+ ```
521
+
522
+ This works on any git host, with your own `git` login: it commits only the locale files on a new
523
+ `livelocale/apply-<time>` branch and pushes it. On GitHub it opens the pull request with `gh`;
524
+ elsewhere it prints the branch to open a pull or merge request from. Every edit is checked again
525
+ against your own files first. For a long link, use `npx livelocale apply --clipboard`, or pass `-` and pipe
526
+ the link in (`npx livelocale apply - < changes.txt`). See [`apply`](#apply) for every option.
527
+
528
+ ### What "Sent" means
529
+
530
+ In this mode "Sent" means the translator handed the link over; it is not a pull request until a developer applies it.
531
+
532
+ ### Hosting notes
533
+
534
+ - **S3 and CloudFront:** the link must end in `index.html`, because an S3 REST origin does not
535
+ resolve folder indexes.
536
+ - **GitHub Pages:** the folder is `livelocale/`, with no underscore for Jekyll to drop, and a
537
+ project site's `/repo/` base path is handled.
538
+ - **Netlify and Cloudflare Pages:** `_headers` must not send DENY (`X-Frame-Options: DENY` or
539
+ `frame-ancestors 'none'`); SAMEORIGIN is fine. Every per-deploy URL is a separate origin with its
540
+ own drafts, so share the branch alias (for example
541
+ `https://staging--site.netlify.app/livelocale/index.html`), not a per-deploy URL.
542
+ - **Basic auth and host password protection** carry over, because the Studio is on the same site.
543
+ - **Vercel:** Deployment Protection must be off for the deployment you share, or the translator
544
+ must have access to it. `doctor --app` reports the login wall.
545
+ - **PWA:** a Workbox service worker (vite-plugin-pwa, workbox-webpack-plugin) can answer the Studio
546
+ with your app shell. Add `navigateFallbackDenylist: [/^\/livelocale\//]` to its Workbox options.
547
+ - **Relative bases** (`base: './'`, CRA `homepage: '.'`): pages served by a client-side route
548
+ fallback cannot load `flag.js`, so pack prints "The app base is relative (./), so flag.js is
549
+ linked per HTML file. Pages served by a client-side route fallback cannot load it; build with
550
+ an absolute base (--base /path/) if the app has such routes."
551
+
552
+ ### Security
553
+
554
+ - **No credential anywhere.** The share link, the build, the host and the browser hold no token.
555
+ Write access stays with the developer's own git login.
556
+ - **A leaked link only proposes text.** A developer still has to apply it, and a person has to
557
+ merge the pull request. `apply` re-validates every edit against your own source locale.
558
+ - **`exposeRepo: false` by default.** The GitHub `owner/repo` is published only with
559
+ `exposeRepo: true` (or `pack --expose-repo`).
560
+ - **The Studio is `noindex`**, so search engines do not list it.
561
+ - **`manifest.json` is public.** `<base>livelocale/manifest.json` makes every string and key name
562
+ public, including keys the app does not show.
563
+ - **The same-origin risk is accepted.** The Studio frames your app on the same origin, so app
564
+ scripts can read and alter drafts. This is accepted because no secret is stored, and you review
565
+ every edit before it is merged.
566
+
567
+ ### Limits
568
+
569
+ - **Desktop only.** Alt+Click needs a keyboard; phones and tablets can only view.
570
+ - **Drafts live in one browser.** Clearing site data, a private window, or Safari's 7-day cap on
571
+ script-writable storage can lose unsent work. Send often, or use **Download backup** in Changes.
572
+ - **One app per repository.**
573
+
349
574
  ## 🌍 Preview mode: editing without a checkout
350
575
 
351
576
  Development mode needs a developer's machine. Preview mode lets a product manager, translator, or
@@ -397,9 +622,9 @@ The app must also let the Studio embed it. Next.js evaluates `headers()` in `nex
397
622
  preview is built, not only at run time.
398
623
 
399
624
  For a Vite app, the Vite plugin mirrors the server's `LIVELOCALE` flag into the page, so
400
- `LIVELOCALE=preview` alone is enough; no `NEXT_PUBLIC_` variable is needed there. The plugin runs
401
- on the Vite dev server only: a static `vite build` served by Apache, Nginx or a CDN has no server for
402
- the Studio to save through, so preview mode does not work there yet.
625
+ `LIVELOCALE=preview` alone is enough; no `NEXT_PUBLIC_` variable is needed there. Preview mode
626
+ runs on the Vite dev server only. For a static `vite build` served by Apache, Nginx or a CDN, use
627
+ the share link instead; see [Share link on any host](#-share-link-on-any-host).
403
628
 
404
629
  Then open the Studio with `?app=https://<preview-url>` (or `studio --app`). It shows a
405
630
  **PREVIEW** badge, asks for the access token, and the Changes panel switches to branch and
@@ -26,6 +26,7 @@ __export(client_exports, {
26
26
  decodeKey: () => decodeKey,
27
27
  encodeKey: () => encodeKey,
28
28
  hasWatermark: () => hasWatermark,
29
+ removeWatermarks: () => removeWatermarks,
29
30
  sanitizeZeroWidth: () => sanitizeZeroWidth,
30
31
  stripWatermark: () => stripWatermark,
31
32
  unwrap: () => unwrap,
@@ -35,10 +36,12 @@ __export(client_exports, {
35
36
  module.exports = __toCommonJS(client_exports);
36
37
 
37
38
  // src/client/watermark.ts
38
- var DELIM = "\u200D";
39
+ var DELIM = "\u200C";
39
40
  var BIT_0 = "\u200B";
40
- var BIT_1 = "\u200C";
41
+ var BIT_1 = "\u200D";
42
+ var WATERMARK_DELIMITER = DELIM;
41
43
  var ZW_RE = /[\u200B\u200C\u200D]/g;
44
+ var WATERMARK_RE = /\u200C(?:[\u200B\u200D]{8})+\u200C/g;
42
45
  function encodeKey(text, key) {
43
46
  const bytes = new TextEncoder().encode(key);
44
47
  let bits = "";
@@ -85,6 +88,15 @@ function stripWatermark(text) {
85
88
  function sanitizeZeroWidth(text) {
86
89
  return text.replace(ZW_RE, "");
87
90
  }
91
+ function removeWatermarks(text) {
92
+ return text.replace(WATERMARK_RE, "");
93
+ }
94
+ function containsWatermark(text) {
95
+ WATERMARK_RE.lastIndex = 0;
96
+ const found = WATERMARK_RE.test(text);
97
+ WATERMARK_RE.lastIndex = 0;
98
+ return found;
99
+ }
88
100
  function hasWatermark(text) {
89
101
  return text.startsWith(DELIM);
90
102
  }
@@ -181,6 +193,121 @@ function isLiveLocaleEnabled() {
181
193
  return evaluate().mode !== "off";
182
194
  }
183
195
 
196
+ // src/client/iframe-bridge.ts
197
+ var DEFAULT_STUDIO_ORIGIN = "http://localhost:3010";
198
+ var STUDIO_ORIGIN_STORAGE_KEY = "livelocale-studio-origin";
199
+ function toHttpOrigin(value) {
200
+ if (!value) return null;
201
+ try {
202
+ const url = new URL(value);
203
+ return url.protocol === "http:" || url.protocol === "https:" ? url.origin : null;
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+ function readSameOriginParent(win) {
209
+ try {
210
+ const parent = win.parent;
211
+ if (!parent || parent === win) return null;
212
+ const own = toHttpOrigin(win.location.origin);
213
+ if (!own) return null;
214
+ return parent.location.origin === own ? own : null;
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+ function resolveStudioOrigin(search, storage, sameOriginParent) {
220
+ const fromParent = toHttpOrigin(sameOriginParent);
221
+ if (fromParent) return fromParent;
222
+ const fromQuery = toHttpOrigin(new URLSearchParams(search).get("livelocale-origin"));
223
+ if (fromQuery) {
224
+ try {
225
+ storage?.setItem(STUDIO_ORIGIN_STORAGE_KEY, fromQuery);
226
+ } catch {
227
+ }
228
+ return fromQuery;
229
+ }
230
+ let remembered = null;
231
+ try {
232
+ remembered = toHttpOrigin(storage?.getItem(STUDIO_ORIGIN_STORAGE_KEY));
233
+ } catch {
234
+ }
235
+ return remembered ?? DEFAULT_STUDIO_ORIGIN;
236
+ }
237
+ var MAX_VISIBLE_TEXTS = 100;
238
+ var MAX_VISIBLE_TEXT_LENGTH = 200;
239
+ var KEY_PATTERN = /^[a-zA-Z0-9._-]+$/;
240
+ var IframeBridge = class {
241
+ allowedOrigin;
242
+ constructor(allowedOrigin) {
243
+ this.allowedOrigin = allowedOrigin;
244
+ }
245
+ /**
246
+ * Sends a message from the iframe client to the parent Studio window.
247
+ */
248
+ sendToParent(type, payload) {
249
+ if (typeof window === "undefined" || !window.parent || window.parent === window) {
250
+ return;
251
+ }
252
+ if (type === "ELEMENT_SELECTED") {
253
+ const p = payload;
254
+ if (!p || typeof p.key !== "string" || p.key.trim() === "" || !/^[a-zA-Z0-9._-]+$/.test(p.key) || typeof p.currentValue !== "string") {
255
+ return;
256
+ }
257
+ }
258
+ if (type === "VISIBLE_KEYS_CHANGED") {
259
+ if (!Array.isArray(payload)) return;
260
+ const validKeys = payload.filter(
261
+ (k) => typeof k === "string" && /^[a-zA-Z0-9._-]+$/.test(k)
262
+ );
263
+ payload = validKeys;
264
+ }
265
+ if (type === "VISIBLE_TEXTS_CHANGED") {
266
+ if (!Array.isArray(payload)) return;
267
+ const texts = [];
268
+ for (const entry of payload) {
269
+ if (texts.length >= MAX_VISIBLE_TEXTS) break;
270
+ if (!entry || typeof entry !== "object") continue;
271
+ const { key, text } = entry;
272
+ if (typeof key !== "string" || !KEY_PATTERN.test(key) || typeof text !== "string") {
273
+ continue;
274
+ }
275
+ texts.push({ key, text: text.slice(0, MAX_VISIBLE_TEXT_LENGTH) });
276
+ }
277
+ payload = texts;
278
+ }
279
+ const message = {
280
+ source: "livelocale-client",
281
+ type,
282
+ payload
283
+ };
284
+ window.parent.postMessage(message, this.allowedOrigin);
285
+ }
286
+ /**
287
+ * Sets up a listener for messages originating from the parent Studio dashboard.
288
+ */
289
+ listenToParent(callback) {
290
+ if (typeof window === "undefined") {
291
+ return () => {
292
+ };
293
+ }
294
+ const listener = (event) => {
295
+ if (event.origin !== this.allowedOrigin) {
296
+ return;
297
+ }
298
+ if (event.data?.source !== "livelocale-studio") {
299
+ return;
300
+ }
301
+ const { type, payload } = event.data;
302
+ callback(type, payload);
303
+ };
304
+ window.addEventListener("message", listener);
305
+ return () => {
306
+ window.removeEventListener("message", listener);
307
+ };
308
+ }
309
+ };
310
+
184
311
  // src/client/dom-interceptor.ts
185
312
  function debounce(fn, wait) {
186
313
  let timer = null;
@@ -233,6 +360,35 @@ var DOMInterceptor = class {
233
360
  });
234
361
  return Array.from(keys);
235
362
  }
363
+ /**
364
+ * Returns one `{ key, text }` per translation key on the page, in document
365
+ * order, with the text it shows now (watermark removed, trimmed, cut to
366
+ * `MAX_VISIBLE_TEXT_LENGTH`). The first element carrying a key wins; its
367
+ * text is the first descendant text node whose watermark names that key.
368
+ * Elements without such a node, and blank texts, are skipped. At most
369
+ * `limit` entries are returned.
370
+ */
371
+ getVisibleTexts(limit = MAX_VISIBLE_TEXTS) {
372
+ const texts = [];
373
+ const seen = /* @__PURE__ */ new Set();
374
+ const elements = document.querySelectorAll("[data-i18n-key]");
375
+ for (const el of elements) {
376
+ if (texts.length >= limit) break;
377
+ const key = el.getAttribute("data-i18n-key");
378
+ if (!key || seen.has(key)) continue;
379
+ seen.add(key);
380
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
381
+ let node;
382
+ while ((node = walker.nextNode()) !== null) {
383
+ const decoded = decodeKey(node.nodeValue ?? "");
384
+ if (decoded?.key !== key) continue;
385
+ const text = decoded.cleanText.trim().slice(0, MAX_VISIBLE_TEXT_LENGTH);
386
+ if (text) texts.push({ key, text });
387
+ break;
388
+ }
389
+ }
390
+ return texts;
391
+ }
236
392
  /**
237
393
  * Initializes the DOM event interceptors in development mode.
238
394
  * Also launches the watermark scanner and MutationObserver.
@@ -403,7 +559,7 @@ var DOMInterceptor = class {
403
559
  }
404
560
  processTextNode(node) {
405
561
  const raw = node.nodeValue ?? "";
406
- if (!raw.includes("\u200D")) return;
562
+ if (!raw.includes(WATERMARK_DELIMITER)) return;
407
563
  const decoded = decodeKey(raw);
408
564
  if (!decoded) return;
409
565
  const parent = node.parentElement;
@@ -476,10 +632,10 @@ var DOMInterceptor = class {
476
632
  Object.defineProperty(proto, "value", {
477
633
  get() {
478
634
  const raw = original.get.call(this);
479
- return sanitizeZeroWidth(raw);
635
+ return removeWatermarks(raw);
480
636
  },
481
637
  set(val) {
482
- const clean = typeof val === "string" ? sanitizeZeroWidth(val) : val;
638
+ const clean = typeof val === "string" ? removeWatermarks(val) : val;
483
639
  original.set.call(this, clean);
484
640
  },
485
641
  configurable: true
@@ -490,7 +646,7 @@ var DOMInterceptor = class {
490
646
  if (!form) return;
491
647
  const inputs = form.querySelectorAll("input, textarea");
492
648
  inputs.forEach((input) => {
493
- const cleaned = sanitizeZeroWidth(input.value);
649
+ const cleaned = removeWatermarks(input.value);
494
650
  if (cleaned !== input.value) {
495
651
  const proto = input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
496
652
  const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
@@ -581,17 +737,17 @@ var DOMInterceptor = class {
581
737
  };
582
738
  handleCopy = (event) => {
583
739
  const selection = window.getSelection()?.toString() || "";
584
- if (selection.includes("\u200D") || selection.includes("\u200B") || selection.includes("\u200C")) {
585
- const clean = sanitizeZeroWidth(selection);
740
+ if (containsWatermark(selection)) {
741
+ const clean = removeWatermarks(selection);
586
742
  event.clipboardData?.setData("text/plain", clean);
587
743
  event.preventDefault();
588
744
  }
589
745
  };
590
746
  handlePaste = (event) => {
591
747
  const text = event.clipboardData?.getData("text/plain") || "";
592
- if (text.includes("\u200D") || text.includes("\u200B") || text.includes("\u200C")) {
748
+ if (containsWatermark(text)) {
593
749
  event.preventDefault();
594
- const cleaned = sanitizeZeroWidth(text);
750
+ const cleaned = removeWatermarks(text);
595
751
  const targetInput = event.target;
596
752
  if (targetInput && "value" in targetInput) {
597
753
  const proto = targetInput instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
@@ -610,64 +766,6 @@ var DOMInterceptor = class {
610
766
  };
611
767
  };
612
768
 
613
- // src/client/iframe-bridge.ts
614
- var IframeBridge = class {
615
- allowedOrigin;
616
- constructor(allowedOrigin) {
617
- this.allowedOrigin = allowedOrigin;
618
- }
619
- /**
620
- * Sends a message from the iframe client to the parent Studio window.
621
- */
622
- sendToParent(type, payload) {
623
- if (typeof window === "undefined" || !window.parent || window.parent === window) {
624
- return;
625
- }
626
- if (type === "ELEMENT_SELECTED") {
627
- const p = payload;
628
- if (!p || typeof p.key !== "string" || p.key.trim() === "" || !/^[a-zA-Z0-9._-]+$/.test(p.key) || typeof p.currentValue !== "string") {
629
- return;
630
- }
631
- }
632
- if (type === "VISIBLE_KEYS_CHANGED") {
633
- if (!Array.isArray(payload)) return;
634
- const validKeys = payload.filter(
635
- (k) => typeof k === "string" && /^[a-zA-Z0-9._-]+$/.test(k)
636
- );
637
- payload = validKeys;
638
- }
639
- const message = {
640
- source: "livelocale-client",
641
- type,
642
- payload
643
- };
644
- window.parent.postMessage(message, this.allowedOrigin);
645
- }
646
- /**
647
- * Sets up a listener for messages originating from the parent Studio dashboard.
648
- */
649
- listenToParent(callback) {
650
- if (typeof window === "undefined") {
651
- return () => {
652
- };
653
- }
654
- const listener = (event) => {
655
- if (event.origin !== this.allowedOrigin) {
656
- return;
657
- }
658
- if (event.data?.source !== "livelocale-studio") {
659
- return;
660
- }
661
- const { type, payload } = event.data;
662
- callback(type, payload);
663
- };
664
- window.addEventListener("message", listener);
665
- return () => {
666
- window.removeEventListener("message", listener);
667
- };
668
- }
669
- };
670
-
671
769
  // src/client/wrap.ts
672
770
  function extractKey(arg, keyPrefix) {
673
771
  if (typeof arg === "string") {
@@ -804,8 +902,16 @@ function restorePreviews() {
804
902
  previewNodes.clear();
805
903
  }
806
904
  if (typeof window !== "undefined" && isLiveLocaleEnabled() && window.parent !== window) {
807
- const urlParams = new URLSearchParams(window.location.search);
808
- const allowedOrigin = urlParams.get("livelocale-origin") || "http://localhost:3010";
905
+ let storage = null;
906
+ try {
907
+ storage = window.sessionStorage;
908
+ } catch {
909
+ }
910
+ const allowedOrigin = resolveStudioOrigin(
911
+ window.location.search,
912
+ storage,
913
+ readSameOriginParent(window)
914
+ );
809
915
  const bridge = new IframeBridge(allowedOrigin);
810
916
  const interceptor = new DOMInterceptor(
811
917
  (payload) => {
@@ -813,6 +919,7 @@ if (typeof window !== "undefined" && isLiveLocaleEnabled() && window.parent !==
813
919
  },
814
920
  (keys) => {
815
921
  bridge.sendToParent("VISIBLE_KEYS_CHANGED", keys);
922
+ bridge.sendToParent("VISIBLE_TEXTS_CHANGED", interceptor.getVisibleTexts());
816
923
  }
817
924
  );
818
925
  interceptor.init();
@@ -844,6 +951,7 @@ if (typeof window !== "undefined" && isLiveLocaleEnabled() && window.parent !==
844
951
  decodeKey,
845
952
  encodeKey,
846
953
  hasWatermark,
954
+ removeWatermarks,
847
955
  sanitizeZeroWidth,
848
956
  stripWatermark,
849
957
  unwrap,