@lionrapid/core 0.5.0 → 0.6.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 (107) hide show
  1. package/dist/base/event-emitter.d.ts.map +1 -1
  2. package/dist/base/event-emitter.js +2 -1
  3. package/dist/base/event-emitter.js.map +1 -1
  4. package/dist/browser/attribute-extractor.d.ts.map +1 -1
  5. package/dist/browser/attribute-extractor.js +2 -1
  6. package/dist/browser/attribute-extractor.js.map +1 -1
  7. package/dist/browser/auto-init.d.ts +64 -0
  8. package/dist/browser/auto-init.d.ts.map +1 -1
  9. package/dist/browser/auto-init.js +171 -59
  10. package/dist/browser/auto-init.js.map +1 -1
  11. package/dist/browser/block-extractor.d.ts.map +1 -1
  12. package/dist/browser/block-extractor.js +3 -1
  13. package/dist/browser/block-extractor.js.map +1 -1
  14. package/dist/browser/editor/editor-api.d.ts +50 -6
  15. package/dist/browser/editor/editor-api.d.ts.map +1 -1
  16. package/dist/browser/editor/editor-api.js +121 -19
  17. package/dist/browser/editor/editor-api.js.map +1 -1
  18. package/dist/browser/editor/editor-init.js +4 -1
  19. package/dist/browser/editor/editor-init.js.map +1 -1
  20. package/dist/browser/editor/sidebar.d.ts +4 -3
  21. package/dist/browser/editor/sidebar.d.ts.map +1 -1
  22. package/dist/browser/editor/sidebar.js +93 -27
  23. package/dist/browser/editor/sidebar.js.map +1 -1
  24. package/dist/browser/editor/snapshot-viewer.d.ts.map +1 -1
  25. package/dist/browser/editor/snapshot-viewer.js +32 -3
  26. package/dist/browser/editor/snapshot-viewer.js.map +1 -1
  27. package/dist/browser/lionrapid.auto.dev.js +424 -147
  28. package/dist/browser/lionrapid.auto.dev.js.map +4 -4
  29. package/dist/browser/lionrapid.auto.js +424 -147
  30. package/dist/browser/lionrapid.auto.js.map +4 -4
  31. package/dist/browser/lionrapid.auto.min.js +4 -3
  32. package/dist/browser/lionrapid.auto.min.js.map +4 -4
  33. package/dist/browser/lionrapid.editor.js +1467 -1115
  34. package/dist/browser/lionrapid.editor.js.map +4 -4
  35. package/dist/browser/lionrapid.editor.min.js +4 -3
  36. package/dist/browser/lionrapid.editor.min.js.map +4 -4
  37. package/dist/browser/lionrapid.esm.js +584 -101
  38. package/dist/browser/lionrapid.esm.js.map +4 -4
  39. package/dist/browser/lionrapid.esm.min.js +3 -2
  40. package/dist/browser/lionrapid.esm.min.js.map +4 -4
  41. package/dist/browser/lionrapid.iife.dev.js +584 -101
  42. package/dist/browser/lionrapid.iife.dev.js.map +4 -4
  43. package/dist/browser/lionrapid.iife.js +584 -101
  44. package/dist/browser/lionrapid.iife.js.map +4 -4
  45. package/dist/browser/lionrapid.iife.min.js +3 -2
  46. package/dist/browser/lionrapid.iife.min.js.map +4 -4
  47. package/dist/browser/lionrapid.umd.js +584 -101
  48. package/dist/browser/lionrapid.umd.js.map +4 -4
  49. package/dist/browser/lionrapid.umd.min.js +3 -2
  50. package/dist/browser/lionrapid.umd.min.js.map +4 -4
  51. package/dist/browser/sanitizer.d.ts +37 -0
  52. package/dist/browser/sanitizer.d.ts.map +1 -0
  53. package/dist/browser/sanitizer.js +93 -0
  54. package/dist/browser/sanitizer.js.map +1 -0
  55. package/dist/builder.d.ts +8 -0
  56. package/dist/builder.d.ts.map +1 -1
  57. package/dist/builder.js +33 -5
  58. package/dist/builder.js.map +1 -1
  59. package/dist/config/lionrapid.config.d.ts +0 -1
  60. package/dist/config/lionrapid.config.d.ts.map +1 -1
  61. package/dist/config/repository.config.d.ts +2 -2
  62. package/dist/config/repository.config.d.ts.map +1 -1
  63. package/dist/content/index.d.ts +3 -0
  64. package/dist/content/index.d.ts.map +1 -1
  65. package/dist/content/index.js +5 -0
  66. package/dist/content/index.js.map +1 -1
  67. package/dist/content/sanitizer/HTMLSanitizer.d.ts +105 -0
  68. package/dist/content/sanitizer/HTMLSanitizer.d.ts.map +1 -0
  69. package/dist/content/sanitizer/HTMLSanitizer.js +257 -0
  70. package/dist/content/sanitizer/HTMLSanitizer.js.map +1 -0
  71. package/dist/content/serializer/HTMLSerializer.d.ts +6 -2
  72. package/dist/content/serializer/HTMLSerializer.d.ts.map +1 -1
  73. package/dist/content/serializer/HTMLSerializer.js +16 -8
  74. package/dist/content/serializer/HTMLSerializer.js.map +1 -1
  75. package/dist/content/splitter/BlockSplitter.d.ts +66 -0
  76. package/dist/content/splitter/BlockSplitter.d.ts.map +1 -0
  77. package/dist/content/splitter/BlockSplitter.js +219 -0
  78. package/dist/content/splitter/BlockSplitter.js.map +1 -0
  79. package/dist/index.d.ts +4 -2
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +17 -1
  82. package/dist/index.js.map +1 -1
  83. package/dist/lionRapid.d.ts +1 -2
  84. package/dist/lionRapid.d.ts.map +1 -1
  85. package/dist/lionRapid.js +5 -7
  86. package/dist/lionRapid.js.map +1 -1
  87. package/dist/repositories/network-repository.d.ts +30 -8
  88. package/dist/repositories/network-repository.d.ts.map +1 -1
  89. package/dist/repositories/network-repository.js +88 -74
  90. package/dist/repositories/network-repository.js.map +1 -1
  91. package/dist/types/lionRapid.types.d.ts +2 -0
  92. package/dist/types/lionRapid.types.d.ts.map +1 -1
  93. package/dist/types/translation.types.d.ts +0 -3
  94. package/dist/types/translation.types.d.ts.map +1 -1
  95. package/dist/utils/context-utils.d.ts +1 -2
  96. package/dist/utils/context-utils.d.ts.map +1 -1
  97. package/dist/utils/context-utils.js +3 -6
  98. package/dist/utils/context-utils.js.map +1 -1
  99. package/dist/utils/locale.d.ts +57 -0
  100. package/dist/utils/locale.d.ts.map +1 -0
  101. package/dist/utils/locale.js +116 -0
  102. package/dist/utils/locale.js.map +1 -0
  103. package/dist/utils/translation-flow.d.ts +1 -1
  104. package/dist/utils/translation-flow.d.ts.map +1 -1
  105. package/dist/utils/translation-flow.js +2 -3
  106. package/dist/utils/translation-flow.js.map +1 -1
  107. package/package.json +2 -1
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- var LionRapid = (() => {
2
+ var __lionrapidAutoExports = (() => {
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -336,6 +336,49 @@ var LionRapid = (() => {
336
336
  packageEmoji: "\u{1F981}"
337
337
  });
338
338
 
339
+ // src/utils/locale.ts
340
+ var LOCALE_TAG_REGEX = /^[a-z]{2,3}(?:-[a-z0-9]{2,4})*$/i;
341
+ var RTL_LANGUAGES = /* @__PURE__ */ new Set(["ar", "fa", "he", "ur", "yi"]);
342
+ function primarySubtag(tag) {
343
+ return tag.split("-")[0].toLowerCase();
344
+ }
345
+ function normalizeLocale(tag) {
346
+ const trimmed = tag.trim();
347
+ if (!trimmed) return trimmed;
348
+ const [lang, ...rest] = trimmed.split("-");
349
+ const out = [lang.toLowerCase()];
350
+ for (const sub of rest) {
351
+ if (sub.length === 4) {
352
+ out.push(sub[0].toUpperCase() + sub.slice(1).toLowerCase());
353
+ } else if (sub.length === 2) {
354
+ out.push(sub.toUpperCase());
355
+ } else {
356
+ out.push(sub.toLowerCase());
357
+ }
358
+ }
359
+ return out.join("-");
360
+ }
361
+ function matchLocale(requested, available, fallback) {
362
+ if (available.length === 0) return fallback;
363
+ const avail = available.map((raw) => ({
364
+ raw,
365
+ lower: normalizeLocale(raw).toLowerCase()
366
+ }));
367
+ for (const req of requested) {
368
+ if (!req) continue;
369
+ const reqLower = normalizeLocale(req).toLowerCase();
370
+ const exact = avail.find((a) => a.lower === reqLower);
371
+ if (exact) return exact.raw;
372
+ const primary = primarySubtag(req);
373
+ const byPrimary = avail.find((a) => a.lower === primary);
374
+ if (byPrimary) return byPrimary.raw;
375
+ }
376
+ return fallback;
377
+ }
378
+ function localeDir(tag) {
379
+ return RTL_LANGUAGES.has(primarySubtag(tag)) ? "rtl" : "ltr";
380
+ }
381
+
339
382
  // src/base/event-emitter.ts
340
383
  var EventEmitter = class {
341
384
  constructor() {
@@ -624,7 +667,7 @@ var LionRapid = (() => {
624
667
  complete: () => {
625
668
  this.emit("language:change:success", {
626
669
  context: this.createContext(requestId, [...baseTrace, "complete"]),
627
- payload: { oldLocale, newLocale }
670
+ payload: { oldLocale, newLocale, dir: localeDir(newLocale) }
628
671
  });
629
672
  },
630
673
  error: (error) => {
@@ -1034,12 +1077,11 @@ var LionRapid = (() => {
1034
1077
  }
1035
1078
 
1036
1079
  // src/utils/context-utils.ts
1037
- function createTranslationContext(parsedKey, currentLocale, defaultNamespace, fallbackLocale) {
1080
+ function createTranslationContext(parsedKey, currentLocale, defaultNamespace) {
1038
1081
  const effectiveNamespace = parsedKey.hasExplicitNamespace ? parsedKey.namespace : defaultNamespace;
1039
1082
  return {
1040
1083
  locale: currentLocale,
1041
- namespace: effectiveNamespace,
1042
- fallbackLocale: fallbackLocale || currentLocale
1084
+ namespace: effectiveNamespace
1043
1085
  };
1044
1086
  }
1045
1087
 
@@ -1156,7 +1198,7 @@ var LionRapid = (() => {
1156
1198
  }
1157
1199
 
1158
1200
  // src/utils/translation-flow.ts
1159
- function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, paramsOrDefaultValue, defaultNamespace, fallbackLocale) {
1201
+ function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, paramsOrDefaultValue, defaultNamespace) {
1160
1202
  const startTime = performance.now();
1161
1203
  const { defaultValue, params } = parseTranslationParams(
1162
1204
  defaultValueOrParams,
@@ -1166,8 +1208,7 @@ var LionRapid = (() => {
1166
1208
  const context = createTranslationContext(
1167
1209
  parsedKey,
1168
1210
  currentLocale,
1169
- defaultNamespace,
1170
- fallbackLocale
1211
+ defaultNamespace
1171
1212
  );
1172
1213
  const individualCacheKey = createIndividualCacheKey(
1173
1214
  context.locale,
@@ -1189,7 +1230,6 @@ var LionRapid = (() => {
1189
1230
  locale: currentLocale,
1190
1231
  namespace: context.namespace,
1191
1232
  // ✅ Use effective namespace (explicit or default)
1192
- fallbackLocale,
1193
1233
  individualCacheKey,
1194
1234
  // ✅ Ready-to-use cache key
1195
1235
  namespaceCacheKey,
@@ -2299,6 +2339,157 @@ var LionRapid = (() => {
2299
2339
  }
2300
2340
  };
2301
2341
 
2342
+ // src/content/sanitizer/HTMLSanitizer.ts
2343
+ var SAFE_TAGS = /* @__PURE__ */ new Set([
2344
+ "a",
2345
+ "abbr",
2346
+ "b",
2347
+ "bdi",
2348
+ "bdo",
2349
+ "br",
2350
+ "cite",
2351
+ "code",
2352
+ "del",
2353
+ "dfn",
2354
+ "em",
2355
+ "i",
2356
+ "img",
2357
+ "ins",
2358
+ "kbd",
2359
+ "mark",
2360
+ "q",
2361
+ "s",
2362
+ "samp",
2363
+ "small",
2364
+ "span",
2365
+ "strong",
2366
+ "sub",
2367
+ "sup",
2368
+ "time",
2369
+ "u",
2370
+ "var",
2371
+ "wbr"
2372
+ ]);
2373
+ var SAFE_ATTRS = /* @__PURE__ */ new Set([
2374
+ "alt",
2375
+ "cite",
2376
+ "class",
2377
+ "datetime",
2378
+ "dir",
2379
+ "height",
2380
+ "href",
2381
+ "id",
2382
+ "lang",
2383
+ "loading",
2384
+ "rel",
2385
+ "role",
2386
+ "src",
2387
+ "target",
2388
+ "title",
2389
+ "width"
2390
+ ]);
2391
+ var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "cite"]);
2392
+ var SAFE_STYLE_PROPS = /* @__PURE__ */ new Set([
2393
+ "color",
2394
+ "background-color",
2395
+ "font-family",
2396
+ "font-size",
2397
+ "font-style",
2398
+ "font-weight",
2399
+ "letter-spacing",
2400
+ "line-height",
2401
+ "text-decoration",
2402
+ "text-decoration-color",
2403
+ "text-decoration-line",
2404
+ "text-decoration-style",
2405
+ "text-transform",
2406
+ "vertical-align",
2407
+ "white-space",
2408
+ "word-spacing"
2409
+ ]);
2410
+ var DANGEROUS_CSS_VALUE = /url\s*\(|expression\s*\(|image-set\s*\(|element\s*\(|paint\s*\(|javascript:|\\|[<>{}@]/i;
2411
+ var SAFE_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
2412
+ var NAMED_ENTITIES = {
2413
+ amp: "&",
2414
+ lt: "<",
2415
+ gt: ">",
2416
+ quot: '"',
2417
+ apos: "'",
2418
+ nbsp: "\xA0",
2419
+ colon: ":",
2420
+ semi: ";",
2421
+ sol: "/",
2422
+ bsol: "\\",
2423
+ Tab: " ",
2424
+ NewLine: "\n"
2425
+ };
2426
+ function decodeEntities2(value) {
2427
+ return value.replace(
2428
+ /&(?:#[xX]([0-9a-fA-F]+);?|#(\d+);?|([a-zA-Z]+);)/g,
2429
+ (full, hex, dec, name) => {
2430
+ if (hex) return String.fromCodePoint(parseInt(hex, 16));
2431
+ if (dec) return String.fromCodePoint(parseInt(dec, 10));
2432
+ return NAMED_ENTITIES[name] ?? full;
2433
+ }
2434
+ );
2435
+ }
2436
+ function escapeHtml(text) {
2437
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2438
+ }
2439
+ function sanitizeUrl(value) {
2440
+ const decoded = decodeEntities2(value);
2441
+ const probe = decoded.replace(/[\u0000-\u0020\u007f]/g, "").toLowerCase();
2442
+ const scheme = /^([a-z][a-z0-9+.-]*):/.exec(probe);
2443
+ if (!scheme) return value;
2444
+ return SAFE_SCHEMES.has(scheme[1]) ? value : null;
2445
+ }
2446
+ function sanitizeStyle(value) {
2447
+ return filterCssDeclarations(value, true);
2448
+ }
2449
+ function filterCssDeclarations(value, requirePropAllowlist) {
2450
+ const decoded = decodeEntities2(value);
2451
+ const kept = [];
2452
+ for (const declaration of decoded.split(";")) {
2453
+ const colon = declaration.indexOf(":");
2454
+ if (colon === -1) continue;
2455
+ const prop = declaration.slice(0, colon).trim().toLowerCase();
2456
+ const val = declaration.slice(colon + 1).trim();
2457
+ if (!/^[a-z-]+$/.test(prop)) continue;
2458
+ if (requirePropAllowlist && !SAFE_STYLE_PROPS.has(prop)) continue;
2459
+ if (val === "" || DANGEROUS_CSS_VALUE.test(val)) continue;
2460
+ kept.push(`${prop}: ${val}`);
2461
+ }
2462
+ return kept.length > 0 ? kept.join("; ") : null;
2463
+ }
2464
+ var ATTR_PATTERN = /([^\s=/>"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]*)))?/g;
2465
+ function sanitizeOpeningTag(rawOpen) {
2466
+ if (!rawOpen.startsWith("<")) return null;
2467
+ const tagName = extractTagName(rawOpen);
2468
+ if (!/^[a-z][a-z0-9-]*$/.test(tagName) || !SAFE_TAGS.has(tagName)) {
2469
+ return null;
2470
+ }
2471
+ const end = rawOpen.endsWith("/>") ? rawOpen.length - 2 : rawOpen.endsWith(">") ? rawOpen.length - 1 : rawOpen.length;
2472
+ const attrSource = rawOpen.slice(1 + tagName.length, end);
2473
+ const attrs = {};
2474
+ for (const match of attrSource.matchAll(ATTR_PATTERN)) {
2475
+ const name = match[1].toLowerCase();
2476
+ const value = match[2] ?? match[3] ?? match[4] ?? "";
2477
+ if (name in attrs) continue;
2478
+ if (name === "style") {
2479
+ const cleanStyle = sanitizeStyle(value);
2480
+ if (cleanStyle !== null) attrs[name] = cleanStyle;
2481
+ continue;
2482
+ }
2483
+ const allowed = SAFE_ATTRS.has(name) || /^data-[a-z0-9-]*$/.test(name) || /^aria-[a-z0-9-]*$/.test(name);
2484
+ if (!allowed) continue;
2485
+ if (URL_ATTRS.has(name)) {
2486
+ if (sanitizeUrl(value) === null) continue;
2487
+ }
2488
+ attrs[name] = value;
2489
+ }
2490
+ return serializeOpeningTag(tagName, attrs);
2491
+ }
2492
+
2302
2493
  // src/content/serializer/HTMLSerializer.ts
2303
2494
  var HTMLSerializer = class {
2304
2495
  serialize(unit) {
@@ -2308,24 +2499,25 @@ var LionRapid = (() => {
2308
2499
  let lastIndex = 0;
2309
2500
  let match;
2310
2501
  while ((match = tagPattern.exec(text)) !== null) {
2311
- result += text.slice(lastIndex, match.index);
2502
+ result += escapeHtml(text.slice(lastIndex, match.index));
2312
2503
  const [fullMatch, closingSlash, indexStr] = match;
2313
2504
  const styleList = styles[indexStr] ?? [];
2314
2505
  const rawOpen = readRawStyle(styleList);
2315
- if (rawOpen) {
2506
+ const safeOpen = rawOpen === null ? null : sanitizeOpeningTag(rawOpen);
2507
+ if (rawOpen && safeOpen !== null) {
2316
2508
  const tagName = extractTagName(rawOpen);
2317
2509
  const isVoid = VOID_TAGS.has(tagName);
2318
2510
  if (closingSlash) {
2319
2511
  if (!isVoid) result += `</${tagName}>`;
2320
2512
  } else {
2321
- result += rawOpen;
2513
+ result += safeOpen;
2322
2514
  }
2323
2515
  } else {
2324
2516
  result += fullMatch;
2325
2517
  }
2326
2518
  lastIndex = match.index + fullMatch.length;
2327
2519
  }
2328
- result += text.slice(lastIndex);
2520
+ result += escapeHtml(text.slice(lastIndex));
2329
2521
  return result;
2330
2522
  }
2331
2523
  };
@@ -2564,10 +2756,9 @@ var LionRapid = (() => {
2564
2756
  var INDEXED_TAG_REGEX = /<\/?[0-9]+\/?>/;
2565
2757
  var HTML_TAG_REGEX = /<[a-z][a-z0-9]*(?:\s+[^>]*)?\/?>/i;
2566
2758
  var LionRapidCore = class {
2567
- constructor(eventEmitter, handlerChain, defaultLocale = "en", fallbackLocale = "en", defaultNamespace) {
2759
+ constructor(eventEmitter, handlerChain, defaultLocale = "en", defaultNamespace) {
2568
2760
  __publicField(this, "handlerChain");
2569
2761
  __publicField(this, "currentLocale");
2570
- __publicField(this, "fallbackLocale");
2571
2762
  __publicField(this, "defaultNamespace");
2572
2763
  __publicField(this, "pendingTranslations", /* @__PURE__ */ new Map());
2573
2764
  __publicField(this, "bus");
@@ -2585,7 +2776,6 @@ var LionRapid = (() => {
2585
2776
  this.bus = eventEmitter;
2586
2777
  this.handlerChain = handlerChain;
2587
2778
  this.currentLocale = defaultLocale;
2588
- this.fallbackLocale = fallbackLocale;
2589
2779
  this.defaultNamespace = defaultNamespace;
2590
2780
  this.cache = new CacheManager(
2591
2781
  this.handlerChain,
@@ -2650,8 +2840,7 @@ var LionRapid = (() => {
2650
2840
  this.currentLocale,
2651
2841
  firstArg,
2652
2842
  secondArg,
2653
- this.defaultNamespace,
2654
- this.fallbackLocale
2843
+ this.defaultNamespace
2655
2844
  );
2656
2845
  logger.debug(`Translation requested: ${request.cleanKey}`);
2657
2846
  const translateBus = this.bus.translation(request);
@@ -2701,8 +2890,7 @@ var LionRapid = (() => {
2701
2890
  this.currentLocale,
2702
2891
  firstArg,
2703
2892
  secondArg,
2704
- this.defaultNamespace,
2705
- this.fallbackLocale
2893
+ this.defaultNamespace
2706
2894
  );
2707
2895
  logger.debug(`HTML translation requested: ${request.cleanKey}`);
2708
2896
  const translateBus = this.bus.translation(request);
@@ -2910,8 +3098,7 @@ var LionRapid = (() => {
2910
3098
  this.currentLocale,
2911
3099
  params,
2912
3100
  defaultValue,
2913
- this.defaultNamespace,
2914
- this.fallbackLocale
3101
+ this.defaultNamespace
2915
3102
  );
2916
3103
  const syncResult = this.handlerChain.handleSync(request);
2917
3104
  const rawValue = syncResult && syncResult.status === 200 ? syncResult.value : request.parsedDefaultValue || request.cleanKey;
@@ -3032,8 +3219,7 @@ var LionRapid = (() => {
3032
3219
  this.currentLocale,
3033
3220
  void 0,
3034
3221
  void 0,
3035
- this.defaultNamespace,
3036
- this.fallbackLocale
3222
+ this.defaultNamespace
3037
3223
  );
3038
3224
  return this.handlerChain.isCached(request);
3039
3225
  }
@@ -3454,6 +3640,9 @@ var LionRapid = (() => {
3454
3640
  };
3455
3641
 
3456
3642
  // src/builder.ts
3643
+ function isLoudPreloadFailure(error) {
3644
+ return !(error instanceof TranslationNotFoundError || error instanceof TranslationLockedError);
3645
+ }
3457
3646
  var LionRapidBuilder = class _LionRapidBuilder {
3458
3647
  constructor(config) {
3459
3648
  __publicField(this, "config");
@@ -3464,7 +3653,6 @@ var LionRapid = (() => {
3464
3653
  __publicField(this, "syncQueue", null);
3465
3654
  this.config = {
3466
3655
  defaultLocale: config.defaultLocale || "en",
3467
- fallbackLocale: config.fallbackLocale || "en",
3468
3656
  namespace: config.namespace || "app"
3469
3657
  };
3470
3658
  }
@@ -3565,7 +3753,6 @@ var LionRapid = (() => {
3565
3753
  eventEmitter,
3566
3754
  handlerChain,
3567
3755
  this.config.defaultLocale,
3568
- this.config.fallbackLocale,
3569
3756
  this.config.namespace
3570
3757
  );
3571
3758
  for (const plugin of this.plugins) {
@@ -3596,7 +3783,17 @@ var LionRapid = (() => {
3596
3783
  logger.debug("Preload targets", {
3597
3784
  namespaces: eagerPreloads.map((c) => `${c.locale}:${c.namespace}`)
3598
3785
  });
3786
+ let warnedLoudly = false;
3787
+ const warnLoudlyOnce = (namespaceKey, error) => {
3788
+ if (!isLoudPreloadFailure(error) || warnedLoudly) return;
3789
+ warnedLoudly = true;
3790
+ console.warn(
3791
+ `[LionRapid] Could not load translations for ${namespaceKey} \u2014 the page stays untranslated until the server is reachable.`,
3792
+ error
3793
+ );
3794
+ };
3599
3795
  const promises = eagerPreloads.map(async (preloadConfig) => {
3796
+ const namespaceKey = `${preloadConfig.locale}:${preloadConfig.namespace}`;
3600
3797
  try {
3601
3798
  const result = await networkHandler.loadNamespace(
3602
3799
  preloadConfig.locale,
@@ -3605,19 +3802,19 @@ var LionRapid = (() => {
3605
3802
  if (result && result.data && result.status === 200) {
3606
3803
  const context = {
3607
3804
  locale: preloadConfig.locale,
3608
- namespace: preloadConfig.namespace,
3609
- fallbackLocale: this.config.fallbackLocale
3805
+ namespace: preloadConfig.namespace
3610
3806
  };
3611
3807
  await handlerChain.saveNamespaceToHandlers(result.data, context);
3612
- logger.debug(
3613
- `Preloaded: ${preloadConfig.locale}:${preloadConfig.namespace}`
3614
- );
3808
+ logger.debug(`Preloaded: ${namespaceKey}`);
3809
+ return;
3810
+ }
3811
+ if (result?.error) {
3812
+ warnLoudlyOnce(namespaceKey, result.error);
3813
+ logger.warn(`Preload failed: ${namespaceKey}`, result.error);
3615
3814
  }
3616
3815
  } catch (error) {
3617
- logger.warn(
3618
- `Preload failed: ${preloadConfig.locale}:${preloadConfig.namespace}`,
3619
- error
3620
- );
3816
+ warnLoudlyOnce(namespaceKey, error);
3817
+ logger.warn(`Preload failed: ${namespaceKey}`, error);
3621
3818
  }
3622
3819
  });
3623
3820
  await Promise.allSettled(promises);
@@ -3631,8 +3828,16 @@ var LionRapid = (() => {
3631
3828
  __publicField(this, "name", "NetworkRepository");
3632
3829
  __publicField(this, "type", "network");
3633
3830
  __publicField(this, "config", {});
3634
- /** Cached version (opaque string from server) per namespace key "locale:namespace" */
3635
- __publicField(this, "namespaceVersions", /* @__PURE__ */ new Map());
3831
+ /**
3832
+ * Namespace snapshot per key "locale:namespace": the server version TOGETHER
3833
+ * with the payload it describes, persisted as ONE localStorage entry.
3834
+ *
3835
+ * Canonical 304 contract (same as edge KV bundle + WP transient cache):
3836
+ * never claim freshness you can't back. If-None-Match is only sent when the
3837
+ * snapshot is in hand; a 304 answers with that snapshot's payload; version
3838
+ * and payload can never desync because they live and die together.
3839
+ */
3840
+ __publicField(this, "namespaceSnapshots", /* @__PURE__ */ new Map());
3636
3841
  /**
3637
3842
  * Default endpoint templates with placeholder support
3638
3843
  * Placeholders: :key, :locale, :namespace
@@ -3745,36 +3950,50 @@ var LionRapid = (() => {
3745
3950
  logger.debug("API base URL is not set");
3746
3951
  throw new NetworkError("API base URL is not set");
3747
3952
  }
3953
+ try {
3954
+ return await this.fetchNamespaceRaw(locale, namespace);
3955
+ } catch (error) {
3956
+ this.handleCaughtError(error);
3957
+ }
3958
+ }
3959
+ /**
3960
+ * Shared namespace fetch with the canonical 304 contract:
3961
+ * - If-None-Match is sent ONLY when a snapshot (version + payload) is in hand.
3962
+ * - 304 answers with that snapshot's payload — never with "nothing".
3963
+ * - A 304 with no snapshot (server/proxy surprise) refetches unconditionally.
3964
+ * - A 200 stores version + payload atomically before either is trusted.
3965
+ */
3966
+ async fetchNamespaceRaw(locale, namespace, allowConditional = true) {
3748
3967
  const url = this.buildUrl("getNamespace", { locale, namespace });
3749
3968
  logger.debug(`API GET Namespace: ${url}`);
3750
3969
  const namespaceKey = `${locale}:${namespace}`;
3751
- try {
3752
- const extraHeaders = {};
3753
- const cachedVersion = this.getVersion(namespaceKey);
3754
- if (cachedVersion) {
3755
- extraHeaders["If-None-Match"] = `W/"${cachedVersion}"`;
3756
- }
3757
- const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
3758
- if (response.status === 304) {
3759
- logger.debug(`Namespace ${namespaceKey}: 304 Not Modified`);
3760
- return null;
3761
- }
3762
- if (!response.ok) {
3763
- this.handleHttpError(response, namespaceKey);
3764
- }
3765
- const data = await response.json();
3766
- logger.debug("API response data:", data);
3767
- if (!data.translations) {
3768
- throw new TranslationNotFoundError(namespaceKey);
3769
- }
3770
- const version = data.meta?.version != null ? String(data.meta.version) : null;
3771
- if (version) {
3772
- this.setVersion(namespaceKey, version);
3970
+ const snapshot = allowConditional ? this.getSnapshot(namespaceKey) : void 0;
3971
+ const extraHeaders = {};
3972
+ if (snapshot) {
3973
+ extraHeaders["If-None-Match"] = `W/"${snapshot.version}"`;
3974
+ }
3975
+ const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
3976
+ if (response.status === 304) {
3977
+ if (snapshot) {
3978
+ logger.debug(`Namespace ${namespaceKey}: 304 Not Modified (snapshot served)`);
3979
+ return snapshot.translations;
3773
3980
  }
3774
- return data.translations;
3775
- } catch (error) {
3776
- this.handleCaughtError(error);
3981
+ logger.debug(`Namespace ${namespaceKey}: unexpected 304 with no snapshot \u2014 refetching`);
3982
+ return this.fetchNamespaceRaw(locale, namespace, false);
3983
+ }
3984
+ if (!response.ok) {
3985
+ this.handleHttpError(response, namespaceKey);
3986
+ }
3987
+ const data = await response.json();
3988
+ logger.debug("API response data:", data);
3989
+ if (!data.translations) {
3990
+ throw new TranslationNotFoundError(namespaceKey);
3991
+ }
3992
+ const version = data.meta?.version != null ? String(data.meta.version) : null;
3993
+ if (version) {
3994
+ this.setSnapshot(namespaceKey, { version, translations: data.translations });
3777
3995
  }
3996
+ return data.translations;
3778
3997
  }
3779
3998
  /**
3780
3999
  * Save translation to API (if supported)
@@ -3859,33 +4078,8 @@ var LionRapid = (() => {
3859
4078
  logger.debug("API base URL is not set");
3860
4079
  throw new NetworkError("API base URL is not set");
3861
4080
  }
3862
- const url = this.buildUrl("getNamespace", { locale, namespace });
3863
- logger.debug(`API GET Namespace ContentUnits: ${url}`);
3864
- const namespaceKey = `${locale}:${namespace}`;
3865
4081
  try {
3866
- const extraHeaders = {};
3867
- const cachedVersion = this.getVersion(namespaceKey);
3868
- if (cachedVersion) {
3869
- extraHeaders["If-None-Match"] = `W/"${cachedVersion}"`;
3870
- }
3871
- const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
3872
- if (response.status === 304) {
3873
- logger.debug(`Namespace ContentUnits ${namespaceKey}: 304 Not Modified`);
3874
- return null;
3875
- }
3876
- if (!response.ok) {
3877
- this.handleHttpError(response, namespaceKey);
3878
- }
3879
- const data = await response.json();
3880
- logger.debug("API response data:", data);
3881
- if (!data.translations) {
3882
- throw new TranslationNotFoundError(namespaceKey);
3883
- }
3884
- const version = data.meta?.version != null ? String(data.meta.version) : null;
3885
- if (version) {
3886
- this.setVersion(namespaceKey, version);
3887
- }
3888
- return data.translations;
4082
+ return await this.fetchNamespaceRaw(locale, namespace);
3889
4083
  } catch (error) {
3890
4084
  this.handleCaughtError(error);
3891
4085
  }
@@ -3995,25 +4189,37 @@ var LionRapid = (() => {
3995
4189
  getConfig() {
3996
4190
  return this.config;
3997
4191
  }
3998
- /** Read persisted version from localStorage, falling back to in-memory map */
3999
- getVersion(namespaceKey) {
4000
- const mem = this.namespaceVersions.get(namespaceKey);
4192
+ /** Read a persisted snapshot (version + payload), falling back to memory.
4193
+ * Legacy bare-version entries and corrupt snapshots are deleted on sight —
4194
+ * a version without its payload must never drive a conditional request. */
4195
+ getSnapshot(namespaceKey) {
4196
+ const mem = this.namespaceSnapshots.get(namespaceKey);
4001
4197
  if (mem) return mem;
4002
4198
  try {
4003
- const stored = localStorage.getItem(_NetworkRepository.VERSION_PREFIX + namespaceKey);
4004
- if (stored) {
4005
- this.namespaceVersions.set(namespaceKey, stored);
4006
- return stored;
4199
+ localStorage.removeItem(_NetworkRepository.LEGACY_VERSION_PREFIX + namespaceKey);
4200
+ const raw = localStorage.getItem(_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey);
4201
+ if (raw) {
4202
+ const parsed = JSON.parse(raw);
4203
+ if (parsed && typeof parsed.version === "string" && parsed.version.length > 0 && parsed.translations && typeof parsed.translations === "object") {
4204
+ this.namespaceSnapshots.set(namespaceKey, parsed);
4205
+ return parsed;
4206
+ }
4207
+ localStorage.removeItem(_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey);
4007
4208
  }
4008
4209
  } catch {
4009
4210
  }
4010
4211
  return void 0;
4011
4212
  }
4012
- /** Persist version to both in-memory map and localStorage */
4013
- setVersion(namespaceKey, version) {
4014
- this.namespaceVersions.set(namespaceKey, version);
4213
+ /** Persist version + payload as ONE entry (memory + localStorage). On quota
4214
+ * failure the previous stored snapshot stays — still a consistent pair. */
4215
+ setSnapshot(namespaceKey, snapshot) {
4216
+ this.namespaceSnapshots.set(namespaceKey, snapshot);
4015
4217
  try {
4016
- localStorage.setItem(_NetworkRepository.VERSION_PREFIX + namespaceKey, version);
4218
+ localStorage.setItem(
4219
+ _NetworkRepository.SNAPSHOT_PREFIX + namespaceKey,
4220
+ JSON.stringify(snapshot)
4221
+ );
4222
+ localStorage.removeItem(_NetworkRepository.LEGACY_VERSION_PREFIX + namespaceKey);
4017
4223
  } catch {
4018
4224
  }
4019
4225
  }
@@ -4102,8 +4308,11 @@ var LionRapid = (() => {
4102
4308
  }
4103
4309
  }
4104
4310
  };
4105
- /** localStorage key prefix for persisted versions */
4106
- __publicField(_NetworkRepository, "VERSION_PREFIX", "lionrapid:v:");
4311
+ /** localStorage key prefix for persisted snapshots (version + payload) */
4312
+ __publicField(_NetworkRepository, "SNAPSHOT_PREFIX", "lionrapid:ns:");
4313
+ /** Legacy bare-version prefix — a version WITHOUT its payload is exactly the
4314
+ * desync the snapshot store exists to prevent; entries are deleted on sight. */
4315
+ __publicField(_NetworkRepository, "LEGACY_VERSION_PREFIX", "lionrapid:v:");
4107
4316
  var NetworkRepository = _NetworkRepository;
4108
4317
 
4109
4318
  // src/browser/exclude-filter.ts
@@ -4496,7 +4705,7 @@ var LionRapid = (() => {
4496
4705
  isInsideSkipTag(element) {
4497
4706
  let current = element.parentElement;
4498
4707
  while (current) {
4499
- if (SKIP_TAGS.has(current.tagName)) {
4708
+ if (SKIP_TAGS.has(current.tagName.toUpperCase())) {
4500
4709
  return true;
4501
4710
  }
4502
4711
  current = current.parentElement;
@@ -4610,7 +4819,7 @@ var LionRapid = (() => {
4610
4819
  isInsideSkipTag(element) {
4611
4820
  let current = element.parentElement;
4612
4821
  while (current) {
4613
- if (SKIP_TAGS2.has(current.tagName)) return true;
4822
+ if (SKIP_TAGS2.has(current.tagName.toUpperCase())) return true;
4614
4823
  current = current.parentElement;
4615
4824
  }
4616
4825
  return false;
@@ -5723,6 +5932,50 @@ var LionRapid = (() => {
5723
5932
  };
5724
5933
 
5725
5934
  // src/browser/auto-init.ts
5935
+ function resolveWritePolicy(apiKey) {
5936
+ const isPublishable = apiKey.startsWith("pk_");
5937
+ return {
5938
+ syncMissing: { enabled: isPublishable },
5939
+ warnReadOnly: !isPublishable
5940
+ };
5941
+ }
5942
+ function configForWindow(config) {
5943
+ return { ...config, apiKey: "" };
5944
+ }
5945
+ function createDeferredLocaleHandler(effects) {
5946
+ let applyLocale = null;
5947
+ let pendingLocale = null;
5948
+ return {
5949
+ onChange: async (locale) => {
5950
+ effects.setCookie(locale);
5951
+ if (effects.navigate) {
5952
+ effects.navigate(locale);
5953
+ return;
5954
+ }
5955
+ if (applyLocale) {
5956
+ await applyLocale(locale);
5957
+ return;
5958
+ }
5959
+ pendingLocale = locale;
5960
+ },
5961
+ attach: (apply) => {
5962
+ applyLocale = apply;
5963
+ if (pendingLocale !== null) {
5964
+ const replay = pendingLocale;
5965
+ pendingLocale = null;
5966
+ void apply(replay).catch(() => {
5967
+ });
5968
+ }
5969
+ }
5970
+ };
5971
+ }
5972
+ function onDomReady(fn) {
5973
+ if (document.readyState === "loading") {
5974
+ document.addEventListener("DOMContentLoaded", fn, { once: true });
5975
+ } else {
5976
+ fn();
5977
+ }
5978
+ }
5726
5979
  function parseConfig(script) {
5727
5980
  const apiKey = script.dataset.apiKey || "";
5728
5981
  const baseUrl = script.dataset.baseUrl || "https://api.lionrapid.com";
@@ -5759,23 +6012,52 @@ var LionRapid = (() => {
5759
6012
  }
5760
6013
  function readLocaleCookie(availableLanguages) {
5761
6014
  if (typeof document === "undefined") return null;
5762
- const match = document.cookie.match(/lionrapid_locale=([a-z]{2})/);
6015
+ const match = document.cookie.match(
6016
+ /lionrapid_locale=([a-z]{2,3}(?:-[a-z0-9]{2,4})*)/i
6017
+ );
5763
6018
  if (!match) return null;
5764
- return availableLanguages.includes(match[1]) ? match[1] : null;
6019
+ return matchLocale([match[1]], availableLanguages, "") || null;
5765
6020
  }
5766
6021
  function detectBrowserLanguage(availableLanguages, defaultLocale) {
5767
6022
  if (typeof navigator === "undefined") return defaultLocale;
5768
- const browserLang = navigator.language || navigator.userLanguage || "";
5769
- const primaryLang = browserLang.split("-")[0].toLowerCase();
5770
- if (availableLanguages.includes(primaryLang)) {
5771
- return primaryLang;
5772
- }
5773
- return defaultLocale;
6023
+ const requested = navigator.languages && navigator.languages.length > 0 ? [...navigator.languages] : [
6024
+ navigator.language || navigator.userLanguage || ""
6025
+ ];
6026
+ return matchLocale(requested, availableLanguages, defaultLocale);
5774
6027
  }
5775
- function navigateToLocale(locale, defaultLocale) {
5776
- const path = window.location.pathname.replace(/^\/([a-z]{2})(\/|$)/, "/");
6028
+ function buildLocaleUrl(pathname, search, locale, defaultLocale, availableLanguages) {
6029
+ const segments = pathname.split("/");
6030
+ const first = segments[1] ?? "";
6031
+ const hasLocalePrefix = LOCALE_TAG_REGEX.test(first) && matchLocale([first], availableLanguages, "") !== "";
6032
+ const path = hasLocalePrefix ? "/" + segments.slice(2).join("/") : pathname;
5777
6033
  const prefix = locale === defaultLocale ? "" : "/" + locale;
5778
- window.location.href = prefix + (path || "/") + window.location.search;
6034
+ return prefix + (path || "/") + search;
6035
+ }
6036
+ function navigateToLocale(locale, defaultLocale, availableLanguages) {
6037
+ window.location.href = buildLocaleUrl(
6038
+ window.location.pathname,
6039
+ window.location.search,
6040
+ locale,
6041
+ defaultLocale,
6042
+ availableLanguages
6043
+ );
6044
+ }
6045
+ function resolveInitialLocale(config) {
6046
+ if (config.mode === "dynamic-only") {
6047
+ const htmlLang = typeof document !== "undefined" ? document.documentElement.lang : "";
6048
+ return htmlLang ? normalizeLocale(htmlLang) : config.defaultLocale;
6049
+ }
6050
+ const cookieLocale = readLocaleCookie(config.languages);
6051
+ if (cookieLocale) return cookieLocale;
6052
+ if (config.autoDetect) {
6053
+ return detectBrowserLanguage(config.languages, config.defaultLocale);
6054
+ }
6055
+ return config.defaultLocale;
6056
+ }
6057
+ function applyLocaleDir(locale) {
6058
+ if (typeof document !== "undefined") {
6059
+ document.documentElement.dir = localeDir(locale);
6060
+ }
5779
6061
  }
5780
6062
  async function autoInit() {
5781
6063
  const scripts = document.querySelectorAll("script[data-api-key]");
@@ -5785,24 +6067,19 @@ var LionRapid = (() => {
5785
6067
  return;
5786
6068
  }
5787
6069
  const config = parseConfig(script);
5788
- window.__lionrapidConfig = config;
6070
+ window.__lionrapidConfig = configForWindow(config);
5789
6071
  if (!config.apiKey) {
5790
6072
  console.warn("[LionRapid] Missing data-api-key attribute");
5791
6073
  return;
5792
6074
  }
5793
- let initialLocale = config.defaultLocale;
5794
- if (config.mode === "dynamic-only") {
5795
- const htmlLang = document.documentElement.lang?.split("-")[0]?.toLowerCase();
5796
- initialLocale = htmlLang || config.defaultLocale;
5797
- } else {
5798
- const cookieLocale = readLocaleCookie(config.languages);
5799
- if (cookieLocale) {
5800
- initialLocale = cookieLocale;
5801
- } else if (config.autoDetect) {
5802
- initialLocale = detectBrowserLanguage(config.languages, config.defaultLocale);
5803
- }
6075
+ const writePolicy = resolveWritePolicy(config.apiKey);
6076
+ if (writePolicy.warnReadOnly) {
6077
+ console.error(
6078
+ "[LionRapid] data-api-key must be a publishable pk_ key; running read-only"
6079
+ );
5804
6080
  }
5805
- const syncMode = config.apiKey.startsWith("pk_") ? "report-only" : "sync";
6081
+ const initialLocale = resolveInitialLocale(config);
6082
+ applyLocaleDir(initialLocale);
5806
6083
  const languageSwitcher = new LanguageSwitcher();
5807
6084
  languageSwitcher.setLanguages(config.languages);
5808
6085
  languageSwitcher.setInitialLocale(initialLocale);
@@ -5828,11 +6105,14 @@ var LionRapid = (() => {
5828
6105
  }
5829
6106
  languageSwitcher.initAll();
5830
6107
  };
5831
- if (document.readyState === "loading") {
5832
- document.addEventListener("DOMContentLoaded", renderSwitcher);
5833
- } else {
5834
- renderSwitcher();
5835
- }
6108
+ onDomReady(renderSwitcher);
6109
+ const localeHandler = createDeferredLocaleHandler({
6110
+ setCookie: (locale) => {
6111
+ document.cookie = `lionrapid_locale=${locale}; path=/; max-age=31536000; SameSite=Lax`;
6112
+ },
6113
+ navigate: config.mode === "dynamic-only" ? (locale) => navigateToLocale(locale, config.defaultLocale, config.languages) : null
6114
+ });
6115
+ languageSwitcher.setOnLanguageChange(localeHandler.onChange);
5836
6116
  try {
5837
6117
  const builder = LionRapidBuilder.init({
5838
6118
  defaultLocale: initialLocale,
@@ -5855,11 +6135,9 @@ var LionRapid = (() => {
5855
6135
  options: {
5856
6136
  baseUrl: config.baseUrl,
5857
6137
  apiKey: config.apiKey,
5858
- syncMissing: {
5859
- enabled: syncMode === "sync",
5860
- debounceMs: 500,
5861
- batchSize: 50
5862
- },
6138
+ // pk_ keys sync missing source strings (discovery); anything else
6139
+ // runs read-only — see resolveWritePolicy.
6140
+ syncMissing: writePolicy.syncMissing,
5863
6141
  preloadNamespaces: [
5864
6142
  {
5865
6143
  locale: initialLocale,
@@ -5880,15 +6158,11 @@ var LionRapid = (() => {
5880
6158
  sourceLocale: config.defaultLocale
5881
6159
  });
5882
6160
  languageSwitcher.setCore(core);
5883
- languageSwitcher.setOnLanguageChange(async (locale) => {
5884
- document.cookie = `lionrapid_locale=${locale}; path=/; max-age=31536000; SameSite=Lax`;
5885
- if (config.mode === "dynamic-only") {
5886
- navigateToLocale(locale, config.defaultLocale);
5887
- return;
5888
- }
6161
+ localeHandler.attach(async (locale) => {
5889
6162
  await core.changeLanguage(locale, {
5890
6163
  preloadNamespaces: [config.namespace]
5891
6164
  });
6165
+ applyLocaleDir(locale);
5892
6166
  autoTranslator.updateLocale(locale);
5893
6167
  autoTranslator.retranslate();
5894
6168
  });
@@ -5935,6 +6209,7 @@ var LionRapid = (() => {
5935
6209
  await core.changeLanguage(locale, {
5936
6210
  preloadNamespaces: [config.namespace]
5937
6211
  });
6212
+ applyLocaleDir(locale);
5938
6213
  autoTranslator.updateLocale(locale);
5939
6214
  autoTranslator.retranslate();
5940
6215
  languageSwitcher.initAll();
@@ -5957,10 +6232,12 @@ var LionRapid = (() => {
5957
6232
  editorScript.async = true;
5958
6233
  document.body.appendChild(editorScript);
5959
6234
  }
5960
- if (config.mode !== "dynamic-only") {
5961
- autoTranslator.translatePage();
5962
- }
5963
- autoTranslator.observe();
6235
+ onDomReady(() => {
6236
+ if (config.mode !== "dynamic-only") {
6237
+ autoTranslator.translatePage();
6238
+ }
6239
+ autoTranslator.observe();
6240
+ });
5964
6241
  } catch (error) {
5965
6242
  console.error("[LionRapid] Initialization failed:", error);
5966
6243
  }