@lionrapid/core 0.3.0 → 0.4.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 (89) hide show
  1. package/dist/browser/attribute-extractor.d.ts +54 -0
  2. package/dist/browser/attribute-extractor.d.ts.map +1 -0
  3. package/dist/browser/attribute-extractor.js +155 -0
  4. package/dist/browser/attribute-extractor.js.map +1 -0
  5. package/dist/browser/auto-init.d.ts +4 -0
  6. package/dist/browser/auto-init.d.ts.map +1 -1
  7. package/dist/browser/auto-init.js +6 -2
  8. package/dist/browser/auto-init.js.map +1 -1
  9. package/dist/browser/auto-translator.d.ts +23 -0
  10. package/dist/browser/auto-translator.d.ts.map +1 -1
  11. package/dist/browser/auto-translator.js +90 -5
  12. package/dist/browser/auto-translator.js.map +1 -1
  13. package/dist/browser/block-extractor.d.ts +8 -0
  14. package/dist/browser/block-extractor.d.ts.map +1 -1
  15. package/dist/browser/block-extractor.js +77 -0
  16. package/dist/browser/block-extractor.js.map +1 -1
  17. package/dist/browser/editor/editor-api.d.ts +22 -1
  18. package/dist/browser/editor/editor-api.d.ts.map +1 -1
  19. package/dist/browser/editor/editor-api.js +54 -19
  20. package/dist/browser/editor/editor-api.js.map +1 -1
  21. package/dist/browser/editor/editor-init.js +72 -3
  22. package/dist/browser/editor/editor-init.js.map +1 -1
  23. package/dist/browser/editor/editor-styles.d.ts +1 -1
  24. package/dist/browser/editor/editor-styles.d.ts.map +1 -1
  25. package/dist/browser/editor/editor-styles.js +38 -1
  26. package/dist/browser/editor/editor-styles.js.map +1 -1
  27. package/dist/browser/editor/inspector.d.ts +8 -0
  28. package/dist/browser/editor/inspector.d.ts.map +1 -1
  29. package/dist/browser/editor/inspector.js +14 -0
  30. package/dist/browser/editor/inspector.js.map +1 -1
  31. package/dist/browser/editor/live-preview.d.ts +1 -1
  32. package/dist/browser/editor/live-preview.d.ts.map +1 -1
  33. package/dist/browser/editor/live-preview.js +27 -16
  34. package/dist/browser/editor/live-preview.js.map +1 -1
  35. package/dist/browser/editor/locale-url.d.ts +18 -0
  36. package/dist/browser/editor/locale-url.d.ts.map +1 -0
  37. package/dist/browser/editor/locale-url.js +37 -0
  38. package/dist/browser/editor/locale-url.js.map +1 -0
  39. package/dist/browser/editor/sidebar.d.ts +6 -0
  40. package/dist/browser/editor/sidebar.d.ts.map +1 -1
  41. package/dist/browser/editor/sidebar.js +133 -4
  42. package/dist/browser/editor/sidebar.js.map +1 -1
  43. package/dist/browser/editor/toolbar.d.ts +8 -1
  44. package/dist/browser/editor/toolbar.d.ts.map +1 -1
  45. package/dist/browser/editor/toolbar.js +49 -4
  46. package/dist/browser/editor/toolbar.js.map +1 -1
  47. package/dist/browser/index.d.ts +2 -0
  48. package/dist/browser/index.d.ts.map +1 -1
  49. package/dist/browser/index.js +1 -0
  50. package/dist/browser/index.js.map +1 -1
  51. package/dist/browser/lionrapid.auto.dev.js +324 -36
  52. package/dist/browser/lionrapid.auto.dev.js.map +4 -4
  53. package/dist/browser/lionrapid.auto.js +324 -36
  54. package/dist/browser/lionrapid.auto.js.map +4 -4
  55. package/dist/browser/lionrapid.auto.min.js +3 -3
  56. package/dist/browser/lionrapid.auto.min.js.map +4 -4
  57. package/dist/browser/lionrapid.editor.js +371 -36
  58. package/dist/browser/lionrapid.editor.js.map +3 -3
  59. package/dist/browser/lionrapid.editor.min.js +41 -4
  60. package/dist/browser/lionrapid.editor.min.js.map +4 -4
  61. package/dist/browser/lionrapid.esm.js +40 -26
  62. package/dist/browser/lionrapid.esm.js.map +2 -2
  63. package/dist/browser/lionrapid.esm.min.js +2 -2
  64. package/dist/browser/lionrapid.esm.min.js.map +3 -3
  65. package/dist/browser/lionrapid.iife.dev.js +40 -26
  66. package/dist/browser/lionrapid.iife.dev.js.map +2 -2
  67. package/dist/browser/lionrapid.iife.js +40 -26
  68. package/dist/browser/lionrapid.iife.js.map +2 -2
  69. package/dist/browser/lionrapid.iife.min.js +2 -2
  70. package/dist/browser/lionrapid.iife.min.js.map +3 -3
  71. package/dist/browser/lionrapid.umd.js +40 -26
  72. package/dist/browser/lionrapid.umd.js.map +2 -2
  73. package/dist/browser/lionrapid.umd.min.js +2 -2
  74. package/dist/browser/lionrapid.umd.min.js.map +3 -3
  75. package/dist/browser/md5.d.ts +3 -2
  76. package/dist/browser/md5.d.ts.map +1 -1
  77. package/dist/browser/md5.js +41 -6
  78. package/dist/browser/md5.js.map +1 -1
  79. package/dist/content/parser/MarkupParser.d.ts +0 -10
  80. package/dist/content/parser/MarkupParser.d.ts.map +1 -1
  81. package/dist/content/parser/MarkupParser.js +45 -3
  82. package/dist/content/parser/MarkupParser.js.map +1 -1
  83. package/dist/content/types.d.ts +30 -0
  84. package/dist/content/types.d.ts.map +1 -1
  85. package/dist/repositories/network-repository.d.ts +0 -14
  86. package/dist/repositories/network-repository.d.ts.map +1 -1
  87. package/dist/repositories/network-repository.js +4 -26
  88. package/dist/repositories/network-repository.js.map +1 -1
  89. package/package.json +1 -1
@@ -23,6 +23,7 @@ var LionRapid = (() => {
23
23
  // src/browser/index.ts
24
24
  var index_exports = {};
25
25
  __export(index_exports, {
26
+ AttributeExtractor: () => AttributeExtractor,
26
27
  AutoTranslator: () => AutoTranslator,
27
28
  BlockExtractor: () => BlockExtractor,
28
29
  CacheStorageRepository: () => CacheStorageRepository,
@@ -2179,10 +2180,44 @@ var LionRapid = (() => {
2179
2180
  }
2180
2181
 
2181
2182
  // src/content/parser/MarkupParser.ts
2183
+ function decodeEntities(text) {
2184
+ const named = {
2185
+ amp: "&",
2186
+ lt: "<",
2187
+ gt: ">",
2188
+ quot: '"',
2189
+ apos: "'",
2190
+ nbsp: " ",
2191
+ rsquo: "\u2019",
2192
+ lsquo: "\u2018",
2193
+ rdquo: "\u201D",
2194
+ ldquo: "\u201C",
2195
+ hellip: "\u2026",
2196
+ mdash: "\u2014",
2197
+ ndash: "\u2013",
2198
+ copy: "\xA9",
2199
+ reg: "\xAE",
2200
+ trade: "\u2122"
2201
+ };
2202
+ return text.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z][a-z0-9]*);/gi, (m, body) => {
2203
+ if (body[0] === "#") {
2204
+ const cp = body[1] === "x" || body[1] === "X" ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
2205
+ if (Number.isFinite(cp) && cp > 0) {
2206
+ try {
2207
+ return String.fromCodePoint(cp);
2208
+ } catch {
2209
+ return m;
2210
+ }
2211
+ }
2212
+ return m;
2213
+ }
2214
+ return named[body.toLowerCase()] ?? m;
2215
+ });
2216
+ }
2182
2217
  var MarkupParser = class {
2183
2218
  parse(input) {
2184
2219
  if (/<\/?[0-9]+>/.test(input)) {
2185
- return { text: input, styles: {} };
2220
+ return { text: decodeEntities(input), styles: {} };
2186
2221
  }
2187
2222
  const styles = {};
2188
2223
  let tagIndex = 0;
@@ -2227,7 +2262,7 @@ var LionRapid = (() => {
2227
2262
  const unclosed = tagStack.pop();
2228
2263
  result += `</${unclosed.index}>`;
2229
2264
  }
2230
- return { text: result, styles };
2265
+ return { text: decodeEntities(result), styles };
2231
2266
  }
2232
2267
  matchTag(input, startPos) {
2233
2268
  const tagStart = input.indexOf("<", startPos);
@@ -3475,7 +3510,7 @@ var LionRapid = (() => {
3475
3510
  logger.debug("API base URL is not set");
3476
3511
  throw new NetworkError("API base URL is not set");
3477
3512
  }
3478
- const url = this.withDraftsParam(this.buildUrl("getTranslation", { key }));
3513
+ const url = this.buildUrl("getTranslation", { key });
3479
3514
  logger.debug(`API GET: ${url}`);
3480
3515
  try {
3481
3516
  const response = await this.makeRequest(url, "GET");
@@ -3511,7 +3546,7 @@ var LionRapid = (() => {
3511
3546
  logger.debug("API base URL is not set");
3512
3547
  throw new NetworkError("API base URL is not set");
3513
3548
  }
3514
- const url = this.withDraftsParam(this.buildUrl("getTranslation", { key }));
3549
+ const url = this.buildUrl("getTranslation", { key });
3515
3550
  logger.debug(`API GET ContentUnit: ${url}`);
3516
3551
  try {
3517
3552
  const response = await this.makeRequest(url, "GET");
@@ -3552,7 +3587,7 @@ var LionRapid = (() => {
3552
3587
  logger.debug("API base URL is not set");
3553
3588
  throw new NetworkError("API base URL is not set");
3554
3589
  }
3555
- const url = this.withDraftsParam(this.buildUrl("getNamespace", { locale, namespace }));
3590
+ const url = this.buildUrl("getNamespace", { locale, namespace });
3556
3591
  logger.debug(`API GET Namespace: ${url}`);
3557
3592
  const namespaceKey = `${locale}:${namespace}`;
3558
3593
  try {
@@ -3666,7 +3701,7 @@ var LionRapid = (() => {
3666
3701
  logger.debug("API base URL is not set");
3667
3702
  throw new NetworkError("API base URL is not set");
3668
3703
  }
3669
- const url = this.withDraftsParam(this.buildUrl("getNamespace", { locale, namespace }));
3704
+ const url = this.buildUrl("getNamespace", { locale, namespace });
3670
3705
  logger.debug(`API GET Namespace ContentUnits: ${url}`);
3671
3706
  const namespaceKey = `${locale}:${namespace}`;
3672
3707
  try {
@@ -3878,26 +3913,6 @@ var LionRapid = (() => {
3878
3913
  }
3879
3914
  return `${baseUrl}${path}`;
3880
3915
  }
3881
- /**
3882
- * Check whether the current page is running inside an active visual-editor
3883
- * session. The editor JS sets `__lr_editor_active=1` as a session cookie
3884
- * when its JWT is established; we read it here so client-side translation
3885
- * fetches request draft content too, matching what the edge proxy does
3886
- * server-side. Outside a browser (Node/SSR) this is always false.
3887
- */
3888
- isEditorPreviewSession() {
3889
- if (typeof document === "undefined") return false;
3890
- return document.cookie.split("; ").some((c) => c === "__lr_editor_active=1");
3891
- }
3892
- /**
3893
- * Append `include_drafts=true` to a built URL when an editor session is
3894
- * active. Leaves the URL unchanged for normal visitors so the delivery
3895
- * gate continues to apply in production.
3896
- */
3897
- withDraftsParam(url) {
3898
- if (!this.isEditorPreviewSession()) return url;
3899
- return url + (url.includes("?") ? "&" : "?") + "include_drafts=true";
3900
- }
3901
3916
  /**
3902
3917
  * Make HTTP request with timeout and headers
3903
3918
  */
@@ -4224,15 +4239,46 @@ var LionRapid = (() => {
4224
4239
  }
4225
4240
  return str;
4226
4241
  }
4227
- function str2binl(str) {
4242
+ function utf8Bytes(str) {
4243
+ if (typeof TextEncoder !== "undefined") {
4244
+ return new TextEncoder().encode(str);
4245
+ }
4246
+ const out = [];
4247
+ for (let i = 0; i < str.length; i++) {
4248
+ const c = str.charCodeAt(i);
4249
+ if (c < 128) {
4250
+ out.push(c);
4251
+ } else if (c < 2048) {
4252
+ out.push(192 | c >> 6, 128 | c & 63);
4253
+ } else if (c >= 55296 && c < 56320 && i + 1 < str.length) {
4254
+ const c2 = str.charCodeAt(++i);
4255
+ const cp = 65536 + (c - 55296 << 10) + (c2 - 56320);
4256
+ out.push(
4257
+ 240 | cp >> 18,
4258
+ 128 | cp >> 12 & 63,
4259
+ 128 | cp >> 6 & 63,
4260
+ 128 | cp & 63
4261
+ );
4262
+ } else {
4263
+ out.push(
4264
+ 224 | c >> 12,
4265
+ 128 | c >> 6 & 63,
4266
+ 128 | c & 63
4267
+ );
4268
+ }
4269
+ }
4270
+ return new Uint8Array(out);
4271
+ }
4272
+ function bytes2binl(bytes) {
4228
4273
  const bin = [];
4229
- for (let i = 0; i < str.length * 8; i += 8) {
4230
- bin[i >> 5] |= (str.charCodeAt(i / 8) & 255) << i % 32;
4274
+ for (let i = 0; i < bytes.length; i++) {
4275
+ bin[i >> 2] |= bytes[i] << i % 4 * 8;
4231
4276
  }
4232
4277
  return bin;
4233
4278
  }
4234
4279
  function md5(str) {
4235
- return binl2hex(binlMD5(str2binl(str), str.length * 8));
4280
+ const bytes = utf8Bytes(str);
4281
+ return binl2hex(binlMD5(bytes2binl(bytes), bytes.length * 8));
4236
4282
  }
4237
4283
  function generateAutoKey(text) {
4238
4284
  const trimmed = text.trim();
@@ -4241,6 +4287,7 @@ var LionRapid = (() => {
4241
4287
 
4242
4288
  // src/browser/block-extractor.ts
4243
4289
  var BLOCK_SELECTOR = "p,h1,h2,h3,h4,h5,h6,li,td,th,label,button,figcaption,dt,dd,summary,caption";
4290
+ var INLINE_BLOCK_SELECTOR = "a";
4244
4291
  var SKIP_TAGS = /* @__PURE__ */ new Set([
4245
4292
  "SCRIPT",
4246
4293
  "STYLE",
@@ -4329,8 +4376,60 @@ var LionRapid = (() => {
4329
4376
  }
4330
4377
  results.push({ element: el, unit, key });
4331
4378
  }
4379
+ this.extractOrphanInline(root, results, includeTranslated);
4332
4380
  return results;
4333
4381
  }
4382
+ /**
4383
+ * Second pass: extract inline elements (currently <a>) that have no block
4384
+ * ancestor. These would otherwise slip through both the block pass (not in
4385
+ * BLOCK_SELECTOR) and the inline-style capture (no parent block to host
4386
+ * them as `<1>...</1>`). Common shape: a `<div>` action group containing
4387
+ * only links.
4388
+ */
4389
+ extractOrphanInline(root, results, includeTranslated) {
4390
+ const candidates = root.querySelectorAll(INLINE_BLOCK_SELECTOR);
4391
+ const elements = [];
4392
+ if (root instanceof Element && root.matches(INLINE_BLOCK_SELECTOR)) {
4393
+ elements.push(root);
4394
+ }
4395
+ for (let i = 0; i < candidates.length; i++) {
4396
+ elements.push(candidates[i]);
4397
+ }
4398
+ for (let i = 0; i < elements.length; i++) {
4399
+ const el = elements[i];
4400
+ if (this.isInsideSkipTag(el)) continue;
4401
+ if (!includeTranslated && this.hasSkipAttribute(el)) continue;
4402
+ if (this.matchesExcludeSelector(el)) continue;
4403
+ if (el.getAttribute("translate") === "no") continue;
4404
+ if (el.parentElement?.closest(BLOCK_SELECTOR)) continue;
4405
+ if (el.parentElement?.closest(INLINE_BLOCK_SELECTOR)) continue;
4406
+ if (el.querySelector(BLOCK_SELECTOR)) continue;
4407
+ const lrOriginal = el.getAttribute("data-lr-original");
4408
+ let unit;
4409
+ if (lrOriginal) {
4410
+ const temp = document.createElement(el.tagName);
4411
+ temp.innerHTML = lrOriginal;
4412
+ unit = parseBlockElement(temp);
4413
+ } else {
4414
+ unit = parseBlockElement(el);
4415
+ }
4416
+ const strippedText = unit.text.replace(/<\/?[0-9]+\/?>/g, "");
4417
+ if (this.excludeFilter.shouldExclude(strippedText)) continue;
4418
+ const lrBlock = el.getAttribute("data-lr-block");
4419
+ let key;
4420
+ if (lrBlock) {
4421
+ key = lrBlock;
4422
+ } else {
4423
+ const dataI18n = el.getAttribute("data-i18n");
4424
+ if (dataI18n) {
4425
+ key = dataI18n.includes(":") ? dataI18n : `common:${dataI18n}`;
4426
+ } else {
4427
+ key = generateAutoKey(unit.text);
4428
+ }
4429
+ }
4430
+ results.push({ element: el, unit, key });
4431
+ }
4432
+ }
4334
4433
  isInsideSkipTag(element) {
4335
4434
  let current = element.parentElement;
4336
4435
  while (current) {
@@ -4366,6 +4465,116 @@ var LionRapid = (() => {
4366
4465
  }
4367
4466
  };
4368
4467
 
4468
+ // src/browser/attribute-extractor.ts
4469
+ var TARGETS = [
4470
+ { selector: 'img[alt], area[alt], input[type="image"][alt]', attr: "alt", type: "alt" },
4471
+ { selector: "[title]", attr: "title", type: "title" },
4472
+ { selector: "input[placeholder], textarea[placeholder]", attr: "placeholder", type: "placeholder" },
4473
+ { selector: 'meta[name="description"][content]', attr: "content", type: "meta-description" },
4474
+ {
4475
+ selector: 'input[type="submit"][value], input[type="button"][value], input[type="reset"][value]',
4476
+ attr: "value",
4477
+ type: "input-value"
4478
+ }
4479
+ ];
4480
+ var SKIP_TAGS2 = /* @__PURE__ */ new Set([
4481
+ "SCRIPT",
4482
+ "STYLE",
4483
+ "CODE",
4484
+ "PRE",
4485
+ "NOSCRIPT",
4486
+ "SVG"
4487
+ ]);
4488
+ var SKIP_ATTRIBUTES2 = [
4489
+ "data-lionrapid-translated",
4490
+ "data-lionrapid-client",
4491
+ "data-no-translate"
4492
+ ];
4493
+ function stampAttrName(attrName) {
4494
+ return `data-lr-attr-${attrName.toLowerCase()}`;
4495
+ }
4496
+ var AttributeExtractor = class {
4497
+ constructor(excludePatterns = {}, excludeSelectors = [], minTextLength = 2) {
4498
+ __publicField(this, "excludeFilter");
4499
+ __publicField(this, "excludeSelectors");
4500
+ this.excludeFilter = new ExcludeFilter(excludePatterns, minTextLength);
4501
+ this.excludeSelectors = excludeSelectors;
4502
+ }
4503
+ /**
4504
+ * Walk the DOM under `root` and return every translatable attribute.
4505
+ *
4506
+ * @param root Element or Document to scan
4507
+ * @param includeStamped When true, elements already carrying
4508
+ * `data-lr-attr-{name}` are returned anyway —
4509
+ * the editor uses this to surface server-handled
4510
+ * attributes for inspection. The runtime keeps
4511
+ * the default `false` so it doesn't re-extract
4512
+ * what the server already keyed.
4513
+ */
4514
+ extract(root, includeStamped = false) {
4515
+ const results = [];
4516
+ for (const target of TARGETS) {
4517
+ const candidates = root.querySelectorAll(target.selector);
4518
+ const all = [];
4519
+ if (root instanceof Element && root.matches(target.selector)) {
4520
+ all.push(root);
4521
+ }
4522
+ for (let i = 0; i < candidates.length; i++) {
4523
+ all.push(candidates[i]);
4524
+ }
4525
+ for (let i = 0; i < all.length; i++) {
4526
+ const el = all[i];
4527
+ if (this.isInsideSkipTag(el)) continue;
4528
+ if (this.hasSkipAncestor(el)) continue;
4529
+ if (this.matchesExcludeSelector(el)) continue;
4530
+ if (el.getAttribute("translate") === "no") continue;
4531
+ const rawValue = el.getAttribute(target.attr);
4532
+ if (rawValue === null) continue;
4533
+ const trimmed = rawValue.trim();
4534
+ if (this.excludeFilter.shouldExclude(trimmed)) continue;
4535
+ const stampName = stampAttrName(target.attr);
4536
+ const stamped = el.getAttribute(stampName);
4537
+ if (!includeStamped && stamped) {
4538
+ continue;
4539
+ }
4540
+ const unit = { text: rawValue, type: target.type };
4541
+ const key = stamped ?? generateAutoKey(rawValue);
4542
+ results.push({ element: el, attrName: target.attr, unit, key });
4543
+ }
4544
+ }
4545
+ return results;
4546
+ }
4547
+ isInsideSkipTag(element) {
4548
+ let current = element.parentElement;
4549
+ while (current) {
4550
+ if (SKIP_TAGS2.has(current.tagName)) return true;
4551
+ current = current.parentElement;
4552
+ }
4553
+ return false;
4554
+ }
4555
+ hasSkipAncestor(element) {
4556
+ let current = element;
4557
+ while (current) {
4558
+ for (const attr of SKIP_ATTRIBUTES2) {
4559
+ if (current.hasAttribute(attr)) return true;
4560
+ }
4561
+ current = current.parentElement;
4562
+ }
4563
+ return false;
4564
+ }
4565
+ matchesExcludeSelector(element) {
4566
+ for (const selector of this.excludeSelectors) {
4567
+ try {
4568
+ if (element.matches(selector) || element.closest(selector)) {
4569
+ return true;
4570
+ }
4571
+ } catch {
4572
+ }
4573
+ }
4574
+ return false;
4575
+ }
4576
+ };
4577
+
4369
4578
  // src/browser/dom-observer.ts
4370
4579
  var DOMObserver = class {
4371
4580
  constructor(callbacks, debounceMs = 50) {
@@ -4470,6 +4679,7 @@ var LionRapid = (() => {
4470
4679
  constructor(config = {}) {
4471
4680
  __publicField(this, "core", null);
4472
4681
  __publicField(this, "blockExtractor");
4682
+ __publicField(this, "attributeExtractor");
4473
4683
  __publicField(this, "domObserver", null);
4474
4684
  __publicField(this, "locale", "");
4475
4685
  __publicField(this, "sourceLocale", "");
@@ -4483,6 +4693,11 @@ var LionRapid = (() => {
4483
4693
  config.excludeSelectors,
4484
4694
  config.minTextLength
4485
4695
  );
4696
+ this.attributeExtractor = new AttributeExtractor(
4697
+ config.excludePatterns,
4698
+ config.excludeSelectors,
4699
+ config.minTextLength
4700
+ );
4486
4701
  this.debounceMs = config.debounceMs ?? 100;
4487
4702
  }
4488
4703
  /**
@@ -4531,8 +4746,9 @@ var LionRapid = (() => {
4531
4746
  if (!this.core) {
4532
4747
  return 0;
4533
4748
  }
4534
- const extracted = this.blockExtractor.extract(element);
4535
- return this.translateBlocks(extracted);
4749
+ const blockCount = this.translateBlocks(this.blockExtractor.extract(element));
4750
+ const attrCount = this.translateAttributesIn(this.attributeExtractor.extract(element));
4751
+ return blockCount + attrCount;
4536
4752
  }
4537
4753
  /**
4538
4754
  * Translate the entire page
@@ -4630,12 +4846,79 @@ var LionRapid = (() => {
4630
4846
  }
4631
4847
  return count;
4632
4848
  }
4849
+ /**
4850
+ * Translate extracted attribute units against the cache.
4851
+ *
4852
+ * Mirrors translateBlocks but for attribute values. For each hit, the
4853
+ * original attribute value is preserved in `data-lr-orig-{name}` so
4854
+ * retranslate() can restore source. The element accumulates a comma-
4855
+ * separated `data-lionrapid-client-attrs` list naming every attr the
4856
+ * client touched — so we can find them all on retranslate without
4857
+ * iterating a fixed selector set.
4858
+ *
4859
+ * Cache misses emit `translation:not-found` with empty styles, so the
4860
+ * existing MissingTranslationQueue + NetworkRepository sync queue
4861
+ * picks them up unchanged.
4862
+ */
4863
+ translateAttributesIn(extracted) {
4864
+ if (!this.core) return 0;
4865
+ let count = 0;
4866
+ for (const { element, attrName, unit, key } of extracted) {
4867
+ const fullKey = `${this.locale}:${this.namespace}:${key}`;
4868
+ const cached = this.core.cache.get(fullKey);
4869
+ if (cached && cached.value !== unit.text && cached.value !== key) {
4870
+ const lower = attrName.toLowerCase();
4871
+ const origAttr = `data-lr-orig-${lower}`;
4872
+ if (!element.hasAttribute(origAttr)) {
4873
+ element.setAttribute(origAttr, unit.text);
4874
+ }
4875
+ element.setAttribute(attrName, cached.value);
4876
+ element.setAttribute(`data-lr-attr-${lower}`, key);
4877
+ const existing = element.getAttribute("data-lionrapid-client-attrs") ?? "";
4878
+ const set = new Set(existing.split(",").filter(Boolean));
4879
+ set.add(lower);
4880
+ element.setAttribute("data-lionrapid-client-attrs", Array.from(set).join(","));
4881
+ count++;
4882
+ } else {
4883
+ this.core.bus.emit("translation:not-found", {
4884
+ payload: {
4885
+ key,
4886
+ fallbackValue: unit.text,
4887
+ locale: this.sourceLocale,
4888
+ namespace: this.namespace,
4889
+ status: 404,
4890
+ styles: {}
4891
+ }
4892
+ });
4893
+ }
4894
+ }
4895
+ return count;
4896
+ }
4633
4897
  /**
4634
4898
  * Get translated count
4635
4899
  */
4636
4900
  getTranslatedCount() {
4637
4901
  return this.translatedCount;
4638
4902
  }
4903
+ /**
4904
+ * Restore every client-translated attribute on `el` from its
4905
+ * `data-lr-orig-*` backup, then strip the stamps. Mirrors the block-side
4906
+ * restoration in retranslate() so language switch / editor reset clears
4907
+ * attributes too.
4908
+ */
4909
+ restoreClientAttributes(el) {
4910
+ const list = el.getAttribute("data-lionrapid-client-attrs");
4911
+ if (list === null) return;
4912
+ for (const name of list.split(",").filter(Boolean)) {
4913
+ const orig = el.getAttribute(`data-lr-orig-${name}`);
4914
+ if (orig !== null) {
4915
+ el.setAttribute(name, orig);
4916
+ el.removeAttribute(`data-lr-orig-${name}`);
4917
+ }
4918
+ el.removeAttribute(`data-lr-attr-${name}`);
4919
+ }
4920
+ el.removeAttribute("data-lionrapid-client-attrs");
4921
+ }
4639
4922
  /**
4640
4923
  * Re-translate a single element (for editor live preview reset)
4641
4924
  */
@@ -4644,14 +4927,15 @@ var LionRapid = (() => {
4644
4927
  if (original !== null) element.innerHTML = original;
4645
4928
  element.removeAttribute("data-lionrapid-client");
4646
4929
  element.removeAttribute("data-lr-block");
4930
+ this.restoreClientAttributes(element);
4647
4931
  this.translateElement(element);
4648
4932
  }
4649
4933
  /**
4650
4934
  * Re-translate the page (after language change)
4651
4935
  */
4652
4936
  retranslate() {
4653
- const translated = document.querySelectorAll("[data-lionrapid-client]");
4654
- translated.forEach((el) => {
4937
+ const translatedBlocks = document.querySelectorAll("[data-lionrapid-client]");
4938
+ translatedBlocks.forEach((el) => {
4655
4939
  const original = el.getAttribute("data-lr-original");
4656
4940
  if (original !== null) {
4657
4941
  el.innerHTML = original;
@@ -4659,6 +4943,8 @@ var LionRapid = (() => {
4659
4943
  el.removeAttribute("data-lionrapid-client");
4660
4944
  el.removeAttribute("data-lr-block");
4661
4945
  });
4946
+ const translatedAttrs = document.querySelectorAll("[data-lionrapid-client-attrs]");
4947
+ translatedAttrs.forEach((el) => this.restoreClientAttributes(el));
4662
4948
  this.translatedCount = 0;
4663
4949
  if (this.locale !== this.sourceLocale) {
4664
4950
  this.translatePage();
@@ -5388,6 +5674,7 @@ var LionRapid = (() => {
5388
5674
  const switcherShowFlags = script.dataset.switcherShowFlags !== "false";
5389
5675
  const switcherShowNames = script.dataset.switcherShowNames === "true";
5390
5676
  const switcherCustomCss = script.dataset.switcherCustomCss || void 0;
5677
+ const editorCdnUrl = script.dataset.editorCdn || void 0;
5391
5678
  return {
5392
5679
  apiKey,
5393
5680
  baseUrl,
@@ -5401,7 +5688,8 @@ var LionRapid = (() => {
5401
5688
  switcherPosition,
5402
5689
  switcherShowFlags,
5403
5690
  switcherShowNames,
5404
- switcherCustomCss
5691
+ switcherCustomCss,
5692
+ editorCdnUrl
5405
5693
  };
5406
5694
  }
5407
5695
  function readLocaleCookie(availableLanguages) {
@@ -5583,7 +5871,7 @@ var LionRapid = (() => {
5583
5871
  };
5584
5872
  window.LionRapid = instance;
5585
5873
  const editorParam = new URLSearchParams(window.location.search).get("__lr_editor");
5586
- if (editorParam && !document.querySelector("script[data-grant]")) {
5874
+ if (editorParam && !document.querySelector("script[data-grant], script[data-lr-editor]")) {
5587
5875
  const editorScript = document.createElement("script");
5588
5876
  editorScript.src = config.editorCdnUrl || "https://cdn.lionrapid.com/lionrapid.editor.min.js";
5589
5877
  editorScript.dataset.grant = editorParam;