@lionrapid/core 0.3.1 → 0.5.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 (101) 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 +9 -0
  6. package/dist/browser/auto-init.d.ts.map +1 -1
  7. package/dist/browser/auto-init.js +28 -4
  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 +494 -109
  52. package/dist/browser/lionrapid.auto.dev.js.map +4 -4
  53. package/dist/browser/lionrapid.auto.js +494 -109
  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 +403 -97
  62. package/dist/browser/lionrapid.esm.js.map +3 -3
  63. package/dist/browser/lionrapid.esm.min.js +2 -2
  64. package/dist/browser/lionrapid.esm.min.js.map +4 -4
  65. package/dist/browser/lionrapid.iife.dev.js +403 -97
  66. package/dist/browser/lionrapid.iife.dev.js.map +3 -3
  67. package/dist/browser/lionrapid.iife.js +403 -97
  68. package/dist/browser/lionrapid.iife.js.map +3 -3
  69. package/dist/browser/lionrapid.iife.min.js +2 -2
  70. package/dist/browser/lionrapid.iife.min.js.map +4 -4
  71. package/dist/browser/lionrapid.umd.js +403 -97
  72. package/dist/browser/lionrapid.umd.js.map +3 -3
  73. package/dist/browser/lionrapid.umd.min.js +2 -2
  74. package/dist/browser/lionrapid.umd.min.js.map +4 -4
  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/builder.d.ts +8 -0
  80. package/dist/builder.d.ts.map +1 -1
  81. package/dist/builder.js +10 -0
  82. package/dist/builder.js.map +1 -1
  83. package/dist/content/parser/MarkupParser.d.ts +0 -10
  84. package/dist/content/parser/MarkupParser.d.ts.map +1 -1
  85. package/dist/content/parser/MarkupParser.js +45 -3
  86. package/dist/content/parser/MarkupParser.js.map +1 -1
  87. package/dist/content/types.d.ts +30 -0
  88. package/dist/content/types.d.ts.map +1 -1
  89. package/dist/index.d.ts +5 -0
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +11 -0
  92. package/dist/index.js.map +1 -1
  93. package/dist/repositories/index.d.ts +1 -0
  94. package/dist/repositories/index.d.ts.map +1 -1
  95. package/dist/repositories/index.js +1 -0
  96. package/dist/repositories/index.js.map +1 -1
  97. package/dist/repositories/static-repository.d.ts +40 -0
  98. package/dist/repositories/static-repository.d.ts.map +1 -0
  99. package/dist/repositories/static-repository.js +60 -0
  100. package/dist/repositories/static-repository.js.map +1 -0
  101. 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);
@@ -3108,6 +3143,155 @@ var LionRapid = (() => {
3108
3143
  }
3109
3144
  };
3110
3145
 
3146
+ // src/repositories/memory-repository.ts
3147
+ var MemoryRepository = class {
3148
+ constructor(config) {
3149
+ __publicField(this, "name", "MemoryRepository");
3150
+ __publicField(this, "type", "memory");
3151
+ __publicField(this, "config", {});
3152
+ __publicField(this, "cache", /* @__PURE__ */ new Map());
3153
+ __publicField(this, "accessOrder", /* @__PURE__ */ new Map());
3154
+ __publicField(this, "maxSize", 1e3);
3155
+ this.config = config;
3156
+ this.maxSize = this.config.options?.maxSize || 1e3;
3157
+ }
3158
+ get(key) {
3159
+ const value = this.cache.get(key);
3160
+ if (value !== void 0) {
3161
+ this.accessOrder.set(key, Date.now());
3162
+ return value;
3163
+ }
3164
+ return null;
3165
+ }
3166
+ set(key, value) {
3167
+ if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
3168
+ this.evictOldest();
3169
+ }
3170
+ this.cache.set(key, value);
3171
+ this.accessOrder.set(key, Date.now());
3172
+ }
3173
+ remove(key) {
3174
+ this.cache.delete(key);
3175
+ this.accessOrder.delete(key);
3176
+ }
3177
+ clear() {
3178
+ this.cache.clear();
3179
+ this.accessOrder.clear();
3180
+ }
3181
+ /**
3182
+ * Clear all entries for a specific namespace
3183
+ * Removes both the namespace cache entry and all individual translation keys
3184
+ */
3185
+ clearNamespace(locale, namespace) {
3186
+ const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
3187
+ const keyPrefix = `${locale}:${namespace}:`;
3188
+ this.cache.delete(namespaceCacheKey);
3189
+ this.accessOrder.delete(namespaceCacheKey);
3190
+ for (const key of this.cache.keys()) {
3191
+ if (key.startsWith(keyPrefix)) {
3192
+ this.cache.delete(key);
3193
+ this.accessOrder.delete(key);
3194
+ }
3195
+ }
3196
+ }
3197
+ getNamespace(locale, namespace) {
3198
+ const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
3199
+ const namespaceData = this.cache.get(namespaceCacheKey);
3200
+ if (!namespaceData) return null;
3201
+ try {
3202
+ return JSON.parse(namespaceData);
3203
+ } catch (error) {
3204
+ this.cache.delete(namespaceCacheKey);
3205
+ return null;
3206
+ }
3207
+ }
3208
+ setNamespace(locale, namespace, translations) {
3209
+ const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
3210
+ this.cache.set(namespaceCacheKey, JSON.stringify(translations));
3211
+ }
3212
+ evictOldest() {
3213
+ let oldestKey = "";
3214
+ let oldestTime = Date.now();
3215
+ for (const [key, time] of this.accessOrder.entries()) {
3216
+ if (time < oldestTime) {
3217
+ oldestTime = time;
3218
+ oldestKey = key;
3219
+ }
3220
+ }
3221
+ if (oldestKey) {
3222
+ this.cache.delete(oldestKey);
3223
+ this.accessOrder.delete(oldestKey);
3224
+ }
3225
+ }
3226
+ /**
3227
+ * Get all cache entries for debugging
3228
+ */
3229
+ getEntries() {
3230
+ const entries = {};
3231
+ for (const [key, value] of this.cache.entries()) {
3232
+ entries[key] = value;
3233
+ }
3234
+ return entries;
3235
+ }
3236
+ getConfig() {
3237
+ return this.config;
3238
+ }
3239
+ };
3240
+
3241
+ // src/repositories/static-repository.ts
3242
+ var StaticRepository = class {
3243
+ constructor(bundle, config = {}) {
3244
+ __publicField(this, "name", "StaticRepository");
3245
+ __publicField(this, "type", "memory");
3246
+ __publicField(this, "config");
3247
+ __publicField(this, "mem");
3248
+ this.config = {
3249
+ enabled: config.enabled ?? true,
3250
+ priority: config.priority ?? 0.5,
3251
+ // after the live memory (0), before storage
3252
+ options: {
3253
+ ttl: Number.MAX_SAFE_INTEGER,
3254
+ maxSize: Number.MAX_SAFE_INTEGER,
3255
+ ...config.options
3256
+ }
3257
+ };
3258
+ this.mem = new MemoryRepository({ options: { maxSize: Number.MAX_SAFE_INTEGER } });
3259
+ for (const [locale, namespaces] of Object.entries(bundle)) {
3260
+ for (const [namespace, translations] of Object.entries(namespaces)) {
3261
+ this.mem.setNamespace(locale, namespace, translations);
3262
+ const flat = flattenTranslations(
3263
+ translations,
3264
+ "",
3265
+ locale,
3266
+ namespace
3267
+ );
3268
+ for (const [key, value] of Object.entries(flat)) this.mem.set(key, value);
3269
+ }
3270
+ }
3271
+ }
3272
+ get(key) {
3273
+ return this.mem.get(key);
3274
+ }
3275
+ getNamespace(locale, namespace) {
3276
+ return this.mem.getNamespace(locale, namespace);
3277
+ }
3278
+ getEntries() {
3279
+ return this.mem.getEntries();
3280
+ }
3281
+ getConfig() {
3282
+ return this.config;
3283
+ }
3284
+ // Read-only: the live memory layer owns fresh network data; this stays pristine.
3285
+ set() {
3286
+ }
3287
+ remove() {
3288
+ }
3289
+ clear() {
3290
+ }
3291
+ setNamespace() {
3292
+ }
3293
+ };
3294
+
3111
3295
  // src/sync/missing-translation-queue.ts
3112
3296
  var COOLDOWN_MS = 6e4;
3113
3297
  var MissingTranslationQueue = class {
@@ -3316,6 +3500,15 @@ var LionRapid = (() => {
3316
3500
  }
3317
3501
  return this;
3318
3502
  }
3503
+ /**
3504
+ * Offline resilience: seed a translation bundle (or the CLI's snapshot files)
3505
+ * as a read-only sync layer that resolves synchronously even with the server
3506
+ * down and nothing cached. Sits after the live memory layer (priority 0) at
3507
+ * `priority` (default 0.5), so fresh network data always shadows it.
3508
+ */
3509
+ useStaticBundle(bundle, priority = 0.5) {
3510
+ return this.use(new StaticRepository(bundle, { priority }));
3511
+ }
3319
3512
  async build() {
3320
3513
  const eventEmitter = new EventEmitter();
3321
3514
  const buildBus = eventEmitter.builder();
@@ -3913,101 +4106,6 @@ var LionRapid = (() => {
3913
4106
  __publicField(_NetworkRepository, "VERSION_PREFIX", "lionrapid:v:");
3914
4107
  var NetworkRepository = _NetworkRepository;
3915
4108
 
3916
- // src/repositories/memory-repository.ts
3917
- var MemoryRepository = class {
3918
- constructor(config) {
3919
- __publicField(this, "name", "MemoryRepository");
3920
- __publicField(this, "type", "memory");
3921
- __publicField(this, "config", {});
3922
- __publicField(this, "cache", /* @__PURE__ */ new Map());
3923
- __publicField(this, "accessOrder", /* @__PURE__ */ new Map());
3924
- __publicField(this, "maxSize", 1e3);
3925
- this.config = config;
3926
- this.maxSize = this.config.options?.maxSize || 1e3;
3927
- }
3928
- get(key) {
3929
- const value = this.cache.get(key);
3930
- if (value !== void 0) {
3931
- this.accessOrder.set(key, Date.now());
3932
- return value;
3933
- }
3934
- return null;
3935
- }
3936
- set(key, value) {
3937
- if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
3938
- this.evictOldest();
3939
- }
3940
- this.cache.set(key, value);
3941
- this.accessOrder.set(key, Date.now());
3942
- }
3943
- remove(key) {
3944
- this.cache.delete(key);
3945
- this.accessOrder.delete(key);
3946
- }
3947
- clear() {
3948
- this.cache.clear();
3949
- this.accessOrder.clear();
3950
- }
3951
- /**
3952
- * Clear all entries for a specific namespace
3953
- * Removes both the namespace cache entry and all individual translation keys
3954
- */
3955
- clearNamespace(locale, namespace) {
3956
- const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
3957
- const keyPrefix = `${locale}:${namespace}:`;
3958
- this.cache.delete(namespaceCacheKey);
3959
- this.accessOrder.delete(namespaceCacheKey);
3960
- for (const key of this.cache.keys()) {
3961
- if (key.startsWith(keyPrefix)) {
3962
- this.cache.delete(key);
3963
- this.accessOrder.delete(key);
3964
- }
3965
- }
3966
- }
3967
- getNamespace(locale, namespace) {
3968
- const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
3969
- const namespaceData = this.cache.get(namespaceCacheKey);
3970
- if (!namespaceData) return null;
3971
- try {
3972
- return JSON.parse(namespaceData);
3973
- } catch (error) {
3974
- this.cache.delete(namespaceCacheKey);
3975
- return null;
3976
- }
3977
- }
3978
- setNamespace(locale, namespace, translations) {
3979
- const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
3980
- this.cache.set(namespaceCacheKey, JSON.stringify(translations));
3981
- }
3982
- evictOldest() {
3983
- let oldestKey = "";
3984
- let oldestTime = Date.now();
3985
- for (const [key, time] of this.accessOrder.entries()) {
3986
- if (time < oldestTime) {
3987
- oldestTime = time;
3988
- oldestKey = key;
3989
- }
3990
- }
3991
- if (oldestKey) {
3992
- this.cache.delete(oldestKey);
3993
- this.accessOrder.delete(oldestKey);
3994
- }
3995
- }
3996
- /**
3997
- * Get all cache entries for debugging
3998
- */
3999
- getEntries() {
4000
- const entries = {};
4001
- for (const [key, value] of this.cache.entries()) {
4002
- entries[key] = value;
4003
- }
4004
- return entries;
4005
- }
4006
- getConfig() {
4007
- return this.config;
4008
- }
4009
- };
4010
-
4011
4109
  // src/browser/exclude-filter.ts
4012
4110
  var DEFAULT_PATTERNS = {
4013
4111
  emails: true,
@@ -4204,15 +4302,46 @@ var LionRapid = (() => {
4204
4302
  }
4205
4303
  return str;
4206
4304
  }
4207
- function str2binl(str) {
4305
+ function utf8Bytes(str) {
4306
+ if (typeof TextEncoder !== "undefined") {
4307
+ return new TextEncoder().encode(str);
4308
+ }
4309
+ const out = [];
4310
+ for (let i = 0; i < str.length; i++) {
4311
+ const c = str.charCodeAt(i);
4312
+ if (c < 128) {
4313
+ out.push(c);
4314
+ } else if (c < 2048) {
4315
+ out.push(192 | c >> 6, 128 | c & 63);
4316
+ } else if (c >= 55296 && c < 56320 && i + 1 < str.length) {
4317
+ const c2 = str.charCodeAt(++i);
4318
+ const cp = 65536 + (c - 55296 << 10) + (c2 - 56320);
4319
+ out.push(
4320
+ 240 | cp >> 18,
4321
+ 128 | cp >> 12 & 63,
4322
+ 128 | cp >> 6 & 63,
4323
+ 128 | cp & 63
4324
+ );
4325
+ } else {
4326
+ out.push(
4327
+ 224 | c >> 12,
4328
+ 128 | c >> 6 & 63,
4329
+ 128 | c & 63
4330
+ );
4331
+ }
4332
+ }
4333
+ return new Uint8Array(out);
4334
+ }
4335
+ function bytes2binl(bytes) {
4208
4336
  const bin = [];
4209
- for (let i = 0; i < str.length * 8; i += 8) {
4210
- bin[i >> 5] |= (str.charCodeAt(i / 8) & 255) << i % 32;
4337
+ for (let i = 0; i < bytes.length; i++) {
4338
+ bin[i >> 2] |= bytes[i] << i % 4 * 8;
4211
4339
  }
4212
4340
  return bin;
4213
4341
  }
4214
4342
  function md5(str) {
4215
- return binl2hex(binlMD5(str2binl(str), str.length * 8));
4343
+ const bytes = utf8Bytes(str);
4344
+ return binl2hex(binlMD5(bytes2binl(bytes), bytes.length * 8));
4216
4345
  }
4217
4346
  function generateAutoKey(text) {
4218
4347
  const trimmed = text.trim();
@@ -4221,6 +4350,7 @@ var LionRapid = (() => {
4221
4350
 
4222
4351
  // src/browser/block-extractor.ts
4223
4352
  var BLOCK_SELECTOR = "p,h1,h2,h3,h4,h5,h6,li,td,th,label,button,figcaption,dt,dd,summary,caption";
4353
+ var INLINE_BLOCK_SELECTOR = "a";
4224
4354
  var SKIP_TAGS = /* @__PURE__ */ new Set([
4225
4355
  "SCRIPT",
4226
4356
  "STYLE",
@@ -4309,8 +4439,60 @@ var LionRapid = (() => {
4309
4439
  }
4310
4440
  results.push({ element: el, unit, key });
4311
4441
  }
4442
+ this.extractOrphanInline(root, results, includeTranslated);
4312
4443
  return results;
4313
4444
  }
4445
+ /**
4446
+ * Second pass: extract inline elements (currently <a>) that have no block
4447
+ * ancestor. These would otherwise slip through both the block pass (not in
4448
+ * BLOCK_SELECTOR) and the inline-style capture (no parent block to host
4449
+ * them as `<1>...</1>`). Common shape: a `<div>` action group containing
4450
+ * only links.
4451
+ */
4452
+ extractOrphanInline(root, results, includeTranslated) {
4453
+ const candidates = root.querySelectorAll(INLINE_BLOCK_SELECTOR);
4454
+ const elements = [];
4455
+ if (root instanceof Element && root.matches(INLINE_BLOCK_SELECTOR)) {
4456
+ elements.push(root);
4457
+ }
4458
+ for (let i = 0; i < candidates.length; i++) {
4459
+ elements.push(candidates[i]);
4460
+ }
4461
+ for (let i = 0; i < elements.length; i++) {
4462
+ const el = elements[i];
4463
+ if (this.isInsideSkipTag(el)) continue;
4464
+ if (!includeTranslated && this.hasSkipAttribute(el)) continue;
4465
+ if (this.matchesExcludeSelector(el)) continue;
4466
+ if (el.getAttribute("translate") === "no") continue;
4467
+ if (el.parentElement?.closest(BLOCK_SELECTOR)) continue;
4468
+ if (el.parentElement?.closest(INLINE_BLOCK_SELECTOR)) continue;
4469
+ if (el.querySelector(BLOCK_SELECTOR)) continue;
4470
+ const lrOriginal = el.getAttribute("data-lr-original");
4471
+ let unit;
4472
+ if (lrOriginal) {
4473
+ const temp = document.createElement(el.tagName);
4474
+ temp.innerHTML = lrOriginal;
4475
+ unit = parseBlockElement(temp);
4476
+ } else {
4477
+ unit = parseBlockElement(el);
4478
+ }
4479
+ const strippedText = unit.text.replace(/<\/?[0-9]+\/?>/g, "");
4480
+ if (this.excludeFilter.shouldExclude(strippedText)) continue;
4481
+ const lrBlock = el.getAttribute("data-lr-block");
4482
+ let key;
4483
+ if (lrBlock) {
4484
+ key = lrBlock;
4485
+ } else {
4486
+ const dataI18n = el.getAttribute("data-i18n");
4487
+ if (dataI18n) {
4488
+ key = dataI18n.includes(":") ? dataI18n : `common:${dataI18n}`;
4489
+ } else {
4490
+ key = generateAutoKey(unit.text);
4491
+ }
4492
+ }
4493
+ results.push({ element: el, unit, key });
4494
+ }
4495
+ }
4314
4496
  isInsideSkipTag(element) {
4315
4497
  let current = element.parentElement;
4316
4498
  while (current) {
@@ -4346,6 +4528,116 @@ var LionRapid = (() => {
4346
4528
  }
4347
4529
  };
4348
4530
 
4531
+ // src/browser/attribute-extractor.ts
4532
+ var TARGETS = [
4533
+ { selector: 'img[alt], area[alt], input[type="image"][alt]', attr: "alt", type: "alt" },
4534
+ { selector: "[title]", attr: "title", type: "title" },
4535
+ { selector: "input[placeholder], textarea[placeholder]", attr: "placeholder", type: "placeholder" },
4536
+ { selector: 'meta[name="description"][content]', attr: "content", type: "meta-description" },
4537
+ {
4538
+ selector: 'input[type="submit"][value], input[type="button"][value], input[type="reset"][value]',
4539
+ attr: "value",
4540
+ type: "input-value"
4541
+ }
4542
+ ];
4543
+ var SKIP_TAGS2 = /* @__PURE__ */ new Set([
4544
+ "SCRIPT",
4545
+ "STYLE",
4546
+ "CODE",
4547
+ "PRE",
4548
+ "NOSCRIPT",
4549
+ "SVG"
4550
+ ]);
4551
+ var SKIP_ATTRIBUTES2 = [
4552
+ "data-lionrapid-translated",
4553
+ "data-lionrapid-client",
4554
+ "data-no-translate"
4555
+ ];
4556
+ function stampAttrName(attrName) {
4557
+ return `data-lr-attr-${attrName.toLowerCase()}`;
4558
+ }
4559
+ var AttributeExtractor = class {
4560
+ constructor(excludePatterns = {}, excludeSelectors = [], minTextLength = 2) {
4561
+ __publicField(this, "excludeFilter");
4562
+ __publicField(this, "excludeSelectors");
4563
+ this.excludeFilter = new ExcludeFilter(excludePatterns, minTextLength);
4564
+ this.excludeSelectors = excludeSelectors;
4565
+ }
4566
+ /**
4567
+ * Walk the DOM under `root` and return every translatable attribute.
4568
+ *
4569
+ * @param root Element or Document to scan
4570
+ * @param includeStamped When true, elements already carrying
4571
+ * `data-lr-attr-{name}` are returned anyway —
4572
+ * the editor uses this to surface server-handled
4573
+ * attributes for inspection. The runtime keeps
4574
+ * the default `false` so it doesn't re-extract
4575
+ * what the server already keyed.
4576
+ */
4577
+ extract(root, includeStamped = false) {
4578
+ const results = [];
4579
+ for (const target of TARGETS) {
4580
+ const candidates = root.querySelectorAll(target.selector);
4581
+ const all = [];
4582
+ if (root instanceof Element && root.matches(target.selector)) {
4583
+ all.push(root);
4584
+ }
4585
+ for (let i = 0; i < candidates.length; i++) {
4586
+ all.push(candidates[i]);
4587
+ }
4588
+ for (let i = 0; i < all.length; i++) {
4589
+ const el = all[i];
4590
+ if (this.isInsideSkipTag(el)) continue;
4591
+ if (this.hasSkipAncestor(el)) continue;
4592
+ if (this.matchesExcludeSelector(el)) continue;
4593
+ if (el.getAttribute("translate") === "no") continue;
4594
+ const rawValue = el.getAttribute(target.attr);
4595
+ if (rawValue === null) continue;
4596
+ const trimmed = rawValue.trim();
4597
+ if (this.excludeFilter.shouldExclude(trimmed)) continue;
4598
+ const stampName = stampAttrName(target.attr);
4599
+ const stamped = el.getAttribute(stampName);
4600
+ if (!includeStamped && stamped) {
4601
+ continue;
4602
+ }
4603
+ const unit = { text: rawValue, type: target.type };
4604
+ const key = stamped ?? generateAutoKey(rawValue);
4605
+ results.push({ element: el, attrName: target.attr, unit, key });
4606
+ }
4607
+ }
4608
+ return results;
4609
+ }
4610
+ isInsideSkipTag(element) {
4611
+ let current = element.parentElement;
4612
+ while (current) {
4613
+ if (SKIP_TAGS2.has(current.tagName)) return true;
4614
+ current = current.parentElement;
4615
+ }
4616
+ return false;
4617
+ }
4618
+ hasSkipAncestor(element) {
4619
+ let current = element;
4620
+ while (current) {
4621
+ for (const attr of SKIP_ATTRIBUTES2) {
4622
+ if (current.hasAttribute(attr)) return true;
4623
+ }
4624
+ current = current.parentElement;
4625
+ }
4626
+ return false;
4627
+ }
4628
+ matchesExcludeSelector(element) {
4629
+ for (const selector of this.excludeSelectors) {
4630
+ try {
4631
+ if (element.matches(selector) || element.closest(selector)) {
4632
+ return true;
4633
+ }
4634
+ } catch {
4635
+ }
4636
+ }
4637
+ return false;
4638
+ }
4639
+ };
4640
+
4349
4641
  // src/browser/dom-observer.ts
4350
4642
  var DOMObserver = class {
4351
4643
  constructor(callbacks, debounceMs = 50) {
@@ -4450,6 +4742,7 @@ var LionRapid = (() => {
4450
4742
  constructor(config = {}) {
4451
4743
  __publicField(this, "core", null);
4452
4744
  __publicField(this, "blockExtractor");
4745
+ __publicField(this, "attributeExtractor");
4453
4746
  __publicField(this, "domObserver", null);
4454
4747
  __publicField(this, "locale", "");
4455
4748
  __publicField(this, "sourceLocale", "");
@@ -4463,6 +4756,11 @@ var LionRapid = (() => {
4463
4756
  config.excludeSelectors,
4464
4757
  config.minTextLength
4465
4758
  );
4759
+ this.attributeExtractor = new AttributeExtractor(
4760
+ config.excludePatterns,
4761
+ config.excludeSelectors,
4762
+ config.minTextLength
4763
+ );
4466
4764
  this.debounceMs = config.debounceMs ?? 100;
4467
4765
  }
4468
4766
  /**
@@ -4511,8 +4809,9 @@ var LionRapid = (() => {
4511
4809
  if (!this.core) {
4512
4810
  return 0;
4513
4811
  }
4514
- const extracted = this.blockExtractor.extract(element);
4515
- return this.translateBlocks(extracted);
4812
+ const blockCount = this.translateBlocks(this.blockExtractor.extract(element));
4813
+ const attrCount = this.translateAttributesIn(this.attributeExtractor.extract(element));
4814
+ return blockCount + attrCount;
4516
4815
  }
4517
4816
  /**
4518
4817
  * Translate the entire page
@@ -4610,12 +4909,79 @@ var LionRapid = (() => {
4610
4909
  }
4611
4910
  return count;
4612
4911
  }
4912
+ /**
4913
+ * Translate extracted attribute units against the cache.
4914
+ *
4915
+ * Mirrors translateBlocks but for attribute values. For each hit, the
4916
+ * original attribute value is preserved in `data-lr-orig-{name}` so
4917
+ * retranslate() can restore source. The element accumulates a comma-
4918
+ * separated `data-lionrapid-client-attrs` list naming every attr the
4919
+ * client touched — so we can find them all on retranslate without
4920
+ * iterating a fixed selector set.
4921
+ *
4922
+ * Cache misses emit `translation:not-found` with empty styles, so the
4923
+ * existing MissingTranslationQueue + NetworkRepository sync queue
4924
+ * picks them up unchanged.
4925
+ */
4926
+ translateAttributesIn(extracted) {
4927
+ if (!this.core) return 0;
4928
+ let count = 0;
4929
+ for (const { element, attrName, unit, key } of extracted) {
4930
+ const fullKey = `${this.locale}:${this.namespace}:${key}`;
4931
+ const cached = this.core.cache.get(fullKey);
4932
+ if (cached && cached.value !== unit.text && cached.value !== key) {
4933
+ const lower = attrName.toLowerCase();
4934
+ const origAttr = `data-lr-orig-${lower}`;
4935
+ if (!element.hasAttribute(origAttr)) {
4936
+ element.setAttribute(origAttr, unit.text);
4937
+ }
4938
+ element.setAttribute(attrName, cached.value);
4939
+ element.setAttribute(`data-lr-attr-${lower}`, key);
4940
+ const existing = element.getAttribute("data-lionrapid-client-attrs") ?? "";
4941
+ const set = new Set(existing.split(",").filter(Boolean));
4942
+ set.add(lower);
4943
+ element.setAttribute("data-lionrapid-client-attrs", Array.from(set).join(","));
4944
+ count++;
4945
+ } else {
4946
+ this.core.bus.emit("translation:not-found", {
4947
+ payload: {
4948
+ key,
4949
+ fallbackValue: unit.text,
4950
+ locale: this.sourceLocale,
4951
+ namespace: this.namespace,
4952
+ status: 404,
4953
+ styles: {}
4954
+ }
4955
+ });
4956
+ }
4957
+ }
4958
+ return count;
4959
+ }
4613
4960
  /**
4614
4961
  * Get translated count
4615
4962
  */
4616
4963
  getTranslatedCount() {
4617
4964
  return this.translatedCount;
4618
4965
  }
4966
+ /**
4967
+ * Restore every client-translated attribute on `el` from its
4968
+ * `data-lr-orig-*` backup, then strip the stamps. Mirrors the block-side
4969
+ * restoration in retranslate() so language switch / editor reset clears
4970
+ * attributes too.
4971
+ */
4972
+ restoreClientAttributes(el) {
4973
+ const list = el.getAttribute("data-lionrapid-client-attrs");
4974
+ if (list === null) return;
4975
+ for (const name of list.split(",").filter(Boolean)) {
4976
+ const orig = el.getAttribute(`data-lr-orig-${name}`);
4977
+ if (orig !== null) {
4978
+ el.setAttribute(name, orig);
4979
+ el.removeAttribute(`data-lr-orig-${name}`);
4980
+ }
4981
+ el.removeAttribute(`data-lr-attr-${name}`);
4982
+ }
4983
+ el.removeAttribute("data-lionrapid-client-attrs");
4984
+ }
4619
4985
  /**
4620
4986
  * Re-translate a single element (for editor live preview reset)
4621
4987
  */
@@ -4624,14 +4990,15 @@ var LionRapid = (() => {
4624
4990
  if (original !== null) element.innerHTML = original;
4625
4991
  element.removeAttribute("data-lionrapid-client");
4626
4992
  element.removeAttribute("data-lr-block");
4993
+ this.restoreClientAttributes(element);
4627
4994
  this.translateElement(element);
4628
4995
  }
4629
4996
  /**
4630
4997
  * Re-translate the page (after language change)
4631
4998
  */
4632
4999
  retranslate() {
4633
- const translated = document.querySelectorAll("[data-lionrapid-client]");
4634
- translated.forEach((el) => {
5000
+ const translatedBlocks = document.querySelectorAll("[data-lionrapid-client]");
5001
+ translatedBlocks.forEach((el) => {
4635
5002
  const original = el.getAttribute("data-lr-original");
4636
5003
  if (original !== null) {
4637
5004
  el.innerHTML = original;
@@ -4639,6 +5006,8 @@ var LionRapid = (() => {
4639
5006
  el.removeAttribute("data-lionrapid-client");
4640
5007
  el.removeAttribute("data-lr-block");
4641
5008
  });
5009
+ const translatedAttrs = document.querySelectorAll("[data-lionrapid-client-attrs]");
5010
+ translatedAttrs.forEach((el) => this.restoreClientAttributes(el));
4642
5011
  this.translatedCount = 0;
4643
5012
  if (this.locale !== this.sourceLocale) {
4644
5013
  this.translatePage();
@@ -5368,6 +5737,8 @@ var LionRapid = (() => {
5368
5737
  const switcherShowFlags = script.dataset.switcherShowFlags !== "false";
5369
5738
  const switcherShowNames = script.dataset.switcherShowNames === "true";
5370
5739
  const switcherCustomCss = script.dataset.switcherCustomCss || void 0;
5740
+ const editorCdnUrl = script.dataset.editorCdn || void 0;
5741
+ const offline = script.dataset.offline !== "false";
5371
5742
  return {
5372
5743
  apiKey,
5373
5744
  baseUrl,
@@ -5381,7 +5752,9 @@ var LionRapid = (() => {
5381
5752
  switcherPosition,
5382
5753
  switcherShowFlags,
5383
5754
  switcherShowNames,
5384
- switcherCustomCss
5755
+ switcherCustomCss,
5756
+ editorCdnUrl,
5757
+ offline
5385
5758
  };
5386
5759
  }
5387
5760
  function readLocaleCookie(availableLanguages) {
@@ -5461,10 +5834,22 @@ var LionRapid = (() => {
5461
5834
  renderSwitcher();
5462
5835
  }
5463
5836
  try {
5464
- const core = await LionRapidBuilder.init({
5837
+ const builder = LionRapidBuilder.init({
5465
5838
  defaultLocale: initialLocale,
5466
5839
  namespace: config.namespace
5467
- }).use(new MemoryRepository({ enabled: true })).use(new LocalStorageRepository({ enabled: true })).use(new CacheStorageRepository({ enabled: true })).use(
5840
+ }).use(new MemoryRepository({ enabled: true })).use(new LocalStorageRepository({ enabled: true })).use(new CacheStorageRepository({ enabled: true }));
5841
+ if (config.offline !== false) {
5842
+ builder.use(
5843
+ new CacheStorageRepository({
5844
+ enabled: true,
5845
+ options: {
5846
+ ttl: Number.MAX_SAFE_INTEGER,
5847
+ cacheName: "lionrapid-offline-v1"
5848
+ }
5849
+ })
5850
+ );
5851
+ }
5852
+ const core = await builder.use(
5468
5853
  new NetworkRepository({
5469
5854
  enabled: true,
5470
5855
  options: {
@@ -5563,7 +5948,7 @@ var LionRapid = (() => {
5563
5948
  };
5564
5949
  window.LionRapid = instance;
5565
5950
  const editorParam = new URLSearchParams(window.location.search).get("__lr_editor");
5566
- if (editorParam && !document.querySelector("script[data-grant]")) {
5951
+ if (editorParam && !document.querySelector("script[data-grant], script[data-lr-editor]")) {
5567
5952
  const editorScript = document.createElement("script");
5568
5953
  editorScript.src = config.editorCdnUrl || "https://cdn.lionrapid.com/lionrapid.editor.min.js";
5569
5954
  editorScript.dataset.grant = editorParam;