@lovalingo/lovalingo 0.5.25 → 0.5.28

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 (67) hide show
  1. package/dist/__tests__/languageFlags.test.d.ts +1 -0
  2. package/dist/__tests__/languageFlags.test.js +42 -0
  3. package/dist/__tests__/mergeEntitlements.test.d.ts +1 -0
  4. package/dist/__tests__/mergeEntitlements.test.js +27 -0
  5. package/dist/components/LanguageSwitcher.js +80 -53
  6. package/dist/components/LovalingoProvider.js +18 -473
  7. package/dist/components/provider/__tests__/seoUtils.test.d.ts +1 -0
  8. package/dist/components/provider/__tests__/seoUtils.test.js +13 -0
  9. package/dist/components/provider/editModeUtils.d.ts +6 -0
  10. package/dist/components/provider/editModeUtils.js +59 -0
  11. package/dist/components/provider/localeUtils.d.ts +8 -0
  12. package/dist/components/provider/localeUtils.js +46 -0
  13. package/dist/components/provider/providerConstants.d.ts +12 -0
  14. package/dist/components/provider/providerConstants.js +11 -0
  15. package/dist/components/provider/seoUtils.d.ts +8 -0
  16. package/dist/components/provider/seoUtils.js +118 -0
  17. package/dist/components/provider/useEditModeOverlay.d.ts +7 -0
  18. package/dist/components/provider/useEditModeOverlay.js +134 -0
  19. package/dist/components/provider/useHistoryNavigationPatch.d.ts +3 -0
  20. package/dist/components/provider/useHistoryNavigationPatch.js +47 -0
  21. package/dist/components/provider/useProviderCache.d.ts +12 -0
  22. package/dist/components/provider/useProviderCache.js +82 -0
  23. package/dist/hooks/provider/useBundleLoading.d.ts +2 -1
  24. package/dist/hooks/provider/useBundleLoading.js +15 -3
  25. package/dist/utils/api.d.ts +3 -78
  26. package/dist/utils/api.js +1 -53
  27. package/dist/utils/apiTypes.d.ts +78 -0
  28. package/dist/utils/apiTypes.js +1 -0
  29. package/dist/utils/apiUtils.d.ts +4 -0
  30. package/dist/utils/apiUtils.js +54 -0
  31. package/dist/utils/languageFlags.d.ts +7 -0
  32. package/dist/utils/languageFlags.js +90 -0
  33. package/dist/utils/markerEngine.d.ts +8 -66
  34. package/dist/utils/markerEngine.js +19 -703
  35. package/dist/utils/markerEngineApply.d.ts +3 -0
  36. package/dist/utils/markerEngineApply.js +136 -0
  37. package/dist/utils/markerEngineConstants.d.ts +10 -0
  38. package/dist/utils/markerEngineConstants.js +12 -0
  39. package/dist/utils/markerEngineCritical.d.ts +2 -0
  40. package/dist/utils/markerEngineCritical.js +98 -0
  41. package/dist/utils/markerEngineDomUtils.d.ts +8 -0
  42. package/dist/utils/markerEngineDomUtils.js +74 -0
  43. package/dist/utils/markerEngineFilters.d.ts +2 -0
  44. package/dist/utils/markerEngineFilters.js +26 -0
  45. package/dist/utils/markerEngineMisses.d.ts +5 -0
  46. package/dist/utils/markerEngineMisses.js +81 -0
  47. package/dist/utils/markerEngineOriginals.d.ts +5 -0
  48. package/dist/utils/markerEngineOriginals.js +29 -0
  49. package/dist/utils/markerEngineScan.d.ts +5 -0
  50. package/dist/utils/markerEngineScan.js +162 -0
  51. package/dist/utils/markerEngineState.d.ts +4 -0
  52. package/dist/utils/markerEngineState.js +14 -0
  53. package/dist/utils/markerEngineStats.d.ts +3 -0
  54. package/dist/utils/markerEngineStats.js +28 -0
  55. package/dist/utils/markerEngineTranslations.d.ts +3 -0
  56. package/dist/utils/markerEngineTranslations.js +49 -0
  57. package/dist/utils/markerEngineTypes.d.ts +62 -0
  58. package/dist/utils/markerEngineTypes.js +1 -0
  59. package/dist/utils/markerEngineViewport.d.ts +2 -0
  60. package/dist/utils/markerEngineViewport.js +27 -0
  61. package/dist/utils/mergeEntitlements.d.ts +2 -0
  62. package/dist/utils/mergeEntitlements.js +7 -0
  63. package/dist/version.d.ts +1 -1
  64. package/dist/version.js +1 -1
  65. package/package.json +1 -1
  66. package/dist/utils/translator.d.ts +0 -80
  67. package/dist/utils/translator.js +0 -802
@@ -1,41 +1,21 @@
1
- import { hashContent } from "./hash";
2
- // Why: keep marker scans cheap while still capturing a small above-the-fold "critical slice" for first paint.
3
- const DEFAULT_THROTTLE_MS = 150;
4
- const DEFAULT_CRITICAL_BUFFER_PX = 200;
5
- const DEFAULT_CRITICAL_MAX = 800;
6
- const EXCLUDE_SELECTOR = "[data-lovalingo-exclude],[data-notranslate],[translate-no],[data-no-translate]";
7
- const UNSAFE_CONTAINER_TAGS = new Set(["script", "style", "noscript", "template", "svg", "canvas"]);
8
- const ATTRIBUTE_MARKS = [
9
- { attr: "title", marker: "data-lovalingo-title-original" },
10
- { attr: "aria-label", marker: "data-lovalingo-aria-label-original" },
11
- { attr: "placeholder", marker: "data-lovalingo-placeholder-original" },
12
- ];
13
- const unsafeSelector = Array.from(UNSAFE_CONTAINER_TAGS).join(",");
1
+ import { DEFAULT_THROTTLE_MS } from "./markerEngineConstants";
2
+ import { applyActiveTranslations, applyTranslationMap, restoreDom } from "./markerEngineApply";
3
+ import { getCriticalFingerprint } from "./markerEngineCritical";
4
+ import { scanDom } from "./markerEngineScan";
5
+ import { scanDomForMisses } from "./markerEngineMisses";
6
+ import { addActiveTranslations, setActiveTranslations } from "./markerEngineTranslations";
7
+ import { buildEmptyStats } from "./markerEngineStats";
8
+ import { getActiveTranslationMap, setCustomExcludeSelector } from "./markerEngineState";
14
9
  let observer = null;
15
10
  let scheduled = null;
16
11
  let running = false;
17
12
  let lastStats = buildEmptyStats();
18
13
  let throttleMs = DEFAULT_THROTTLE_MS;
19
14
  let applying = false;
20
- let customExcludeSelector = null;
21
- let activeTranslationMap = null;
22
- const originalTextByNode = new WeakMap();
23
- const originalAttrByEl = new WeakMap();
24
- function buildEmptyStats() {
25
- return {
26
- totalTextNodes: 0,
27
- markedNodes: 0,
28
- skippedUnsafeNodes: 0,
29
- skippedExcludedNodes: 0,
30
- skippedNonTranslatableNodes: 0,
31
- totalChars: 0,
32
- markedChars: 0,
33
- skippedUnsafeChars: 0,
34
- skippedExcludedChars: 0,
35
- skippedNonTranslatableChars: 0,
36
- coverageRatio: 0,
37
- coverageRatioChars: 0,
38
- };
15
+ function scanDomWithGlobals(opts) {
16
+ const result = scanDom(opts);
17
+ setGlobalStats(result.stats);
18
+ return result;
39
19
  }
40
20
  function setGlobalStats(stats) {
41
21
  lastStats = stats;
@@ -49,422 +29,11 @@ function setGlobalStats(stats) {
49
29
  if (!g.__lovalingo.dom)
50
30
  g.__lovalingo.dom = {};
51
31
  g.__lovalingo.dom.getStats = () => lastStats;
52
- g.__lovalingo.dom.scan = () => scanDom({ maxSegments: 20000, includeCritical: true });
32
+ g.__lovalingo.dom.scan = () => scanDomWithGlobals({ maxSegments: 20000, includeCritical: true });
53
33
  g.__lovalingo.dom.getCriticalFingerprint = () => getCriticalFingerprint();
54
34
  g.__lovalingo.dom.apply = (bundle) => ({ applied: applyTranslationMap(bundle, document.body) });
55
35
  g.__lovalingo.dom.restore = () => restoreDom(document.body);
56
36
  }
57
- function isExcludedElement(el) {
58
- if (!el)
59
- return false;
60
- if (el.closest(EXCLUDE_SELECTOR))
61
- return true;
62
- if (customExcludeSelector) {
63
- try {
64
- if (el.closest(customExcludeSelector))
65
- return true;
66
- }
67
- catch {
68
- // ignore invalid selector strings
69
- }
70
- }
71
- return false;
72
- }
73
- function findUnsafeContainer(el) {
74
- if (!el)
75
- return null;
76
- if (!unsafeSelector)
77
- return null;
78
- return el.closest(unsafeSelector);
79
- }
80
- function getStableKey(el) {
81
- const owner = el.closest("[data-lovalingo-key]");
82
- const key = owner?.getAttribute("data-lovalingo-key") || "";
83
- return key.trim();
84
- }
85
- function getElementIndex(el) {
86
- const parent = el.parentElement;
87
- if (!parent)
88
- return 0;
89
- const children = Array.from(parent.children);
90
- const idx = children.indexOf(el);
91
- return idx >= 0 ? idx : 0;
92
- }
93
- function getTextNodeIndex(node) {
94
- let index = 0;
95
- let prev = node.previousSibling;
96
- while (prev) {
97
- if (prev.nodeType === Node.TEXT_NODE)
98
- index += 1;
99
- prev = prev.previousSibling;
100
- }
101
- return index;
102
- }
103
- function buildElementPath(el) {
104
- const parts = [];
105
- let current = el;
106
- while (current && current.tagName && current !== document.body) {
107
- const tag = current.tagName.toLowerCase();
108
- const idx = getElementIndex(current);
109
- parts.push(`${tag}[${idx}]`);
110
- current = current.parentElement;
111
- }
112
- parts.push("body");
113
- return parts.reverse().join("/");
114
- }
115
- function normalizeWhitespace(value) {
116
- return (value || "").toString().replace(/\s+/g, " ").trim();
117
- }
118
- function isTranslatableText(text) {
119
- if (!text || text.trim().length < 2)
120
- return false;
121
- if (/^(__[A-Z0-9_]+__\s*)+$/.test(text))
122
- return false;
123
- if (/^\d+(\.\d+)?$/.test(text))
124
- return false;
125
- if (!/[a-zA-Z\u00C0-\u024F\u1E00-\u1EFF]/.test(text))
126
- return false;
127
- return true;
128
- }
129
- function buildStableId(el, text, textIndex) {
130
- const key = getStableKey(el);
131
- const path = buildElementPath(el);
132
- const raw = `${path}#text[${textIndex}]|${text.trim()}|${key}`;
133
- return hashContent(raw);
134
- }
135
- function buildSelector(el) {
136
- const id = el.id;
137
- if (id)
138
- return `#${id.replace(/[^a-zA-Z0-9_-]/g, "\\$&")}`;
139
- const className = el.className;
140
- if (typeof className === "string" && className.trim()) {
141
- const classes = className
142
- .split(/\s+/)
143
- .map((c) => c.trim())
144
- .filter(Boolean)
145
- .slice(0, 3)
146
- .map((c) => `.${c.replace(/[^a-zA-Z0-9_-]/g, "\\$&")}`)
147
- .join("");
148
- if (classes)
149
- return classes;
150
- }
151
- return null;
152
- }
153
- function getOrInitTextOriginal(node, parent) {
154
- const existing = originalTextByNode.get(node);
155
- if (existing)
156
- return existing;
157
- const raw = node.nodeValue || "";
158
- const leading = raw.match(/^\s*/)?.[0] ?? "";
159
- const trailing = raw.match(/\s*$/)?.[0] ?? "";
160
- const trimmed = raw.trim();
161
- const id = buildStableId(parent, trimmed, getTextNodeIndex(node));
162
- const created = { raw, trimmed, leading, trailing, id };
163
- originalTextByNode.set(node, created);
164
- return created;
165
- }
166
- function getOrInitAttrOriginal(el, attr) {
167
- let map = originalAttrByEl.get(el);
168
- if (!map) {
169
- map = new Map();
170
- originalAttrByEl.set(el, map);
171
- }
172
- const existing = map.get(attr);
173
- if (existing != null)
174
- return existing;
175
- const value = (el.getAttribute(attr) || "").toString();
176
- map.set(attr, value);
177
- return value;
178
- }
179
- function isInViewport(rect, viewportHeight, bufferPx) {
180
- if (!rect)
181
- return false;
182
- if (!Number.isFinite(rect.top) || !Number.isFinite(rect.bottom))
183
- return false;
184
- if (rect.width <= 0 || rect.height <= 0)
185
- return false;
186
- return rect.bottom > -bufferPx && rect.top < viewportHeight + bufferPx;
187
- }
188
- function getTextNodeRect(node) {
189
- try {
190
- const range = document.createRange();
191
- range.selectNodeContents(node);
192
- const rect = range.getBoundingClientRect();
193
- if (rect && rect.width > 0 && rect.height > 0)
194
- return rect;
195
- }
196
- catch {
197
- // ignore
198
- }
199
- try {
200
- return node.parentElement ? node.parentElement.getBoundingClientRect() : null;
201
- }
202
- catch {
203
- return null;
204
- }
205
- }
206
- function considerTextNode(node, stats, segments, occurrences, seen, maxSegments, critical) {
207
- const raw = node.nodeValue || "";
208
- if (!raw)
209
- return;
210
- const trimmed = raw.trim();
211
- if (!trimmed)
212
- return;
213
- stats.totalTextNodes += 1;
214
- stats.totalChars += raw.length;
215
- const parent = node.parentElement;
216
- if (!parent)
217
- return;
218
- if (isExcludedElement(parent)) {
219
- stats.skippedExcludedNodes += 1;
220
- stats.skippedExcludedChars += raw.length;
221
- return;
222
- }
223
- const unsafe = findUnsafeContainer(parent);
224
- if (unsafe) {
225
- stats.skippedUnsafeNodes += 1;
226
- stats.skippedUnsafeChars += raw.length;
227
- return;
228
- }
229
- if (!isTranslatableText(trimmed)) {
230
- stats.skippedNonTranslatableNodes += 1;
231
- stats.skippedNonTranslatableChars += raw.length;
232
- return;
233
- }
234
- const original = getOrInitTextOriginal(node, parent);
235
- stats.markedNodes += 1;
236
- stats.markedChars += raw.length;
237
- if (segments.length < maxSegments) {
238
- const originalText = normalizeWhitespace(original.trimmed) || null;
239
- const currentText = normalizeWhitespace(node.nodeValue || "") || null;
240
- segments.push({
241
- kind: "text",
242
- selector: buildSelector(parent),
243
- original: originalText,
244
- current: currentText,
245
- html: null,
246
- });
247
- if (originalText && !seen.has(originalText)) {
248
- seen.add(originalText);
249
- occurrences.push({ source_text: originalText, semantic_context: "text" });
250
- }
251
- if (critical?.enabled && originalText && !critical.seen.has(originalText)) {
252
- const rect = getTextNodeRect(node);
253
- if (isInViewport(rect, critical.viewportHeight, critical.bufferPx)) {
254
- critical.seen.add(originalText);
255
- critical.occurrences.push({ source_text: originalText, semantic_context: "critical:text" });
256
- }
257
- }
258
- }
259
- }
260
- function considerAttributes(root, segments, occurrences, seen, maxSegments, critical) {
261
- const nodes = root.querySelectorAll("[title],[aria-label],[placeholder]");
262
- nodes.forEach((el) => {
263
- if (isExcludedElement(el))
264
- return;
265
- if (findUnsafeContainer(el))
266
- return;
267
- for (const { attr } of ATTRIBUTE_MARKS) {
268
- const value = el.getAttribute(attr);
269
- if (!value)
270
- continue;
271
- const trimmed = value.trim();
272
- if (!trimmed || !isTranslatableText(trimmed))
273
- continue;
274
- const original = normalizeWhitespace(getOrInitAttrOriginal(el, attr)) || null;
275
- const current = normalizeWhitespace(el.getAttribute(attr) || "") || null;
276
- const kind = (attr === "title" ? "title" : attr === "aria-label" ? "aria-label" : "placeholder");
277
- if (segments.length < maxSegments) {
278
- segments.push({
279
- kind,
280
- selector: buildSelector(el),
281
- original,
282
- current,
283
- html: null,
284
- });
285
- }
286
- if (original && !seen.has(original)) {
287
- seen.add(original);
288
- occurrences.push({ source_text: original, semantic_context: `attr:${attr}` });
289
- }
290
- if (critical?.enabled && original && !critical.seen.has(original)) {
291
- let rect = null;
292
- try {
293
- rect = el.getBoundingClientRect();
294
- }
295
- catch {
296
- rect = null;
297
- }
298
- if (isInViewport(rect, critical.viewportHeight, critical.bufferPx)) {
299
- critical.seen.add(original);
300
- critical.occurrences.push({ source_text: original, semantic_context: `critical:attr:${attr}` });
301
- }
302
- }
303
- }
304
- });
305
- }
306
- function finalizeStats(stats) {
307
- const eligibleNodes = stats.totalTextNodes -
308
- stats.skippedUnsafeNodes -
309
- stats.skippedExcludedNodes -
310
- stats.skippedNonTranslatableNodes;
311
- const eligibleChars = stats.totalChars -
312
- stats.skippedUnsafeChars -
313
- stats.skippedExcludedChars -
314
- stats.skippedNonTranslatableChars;
315
- stats.coverageRatio = eligibleNodes > 0 ? stats.markedNodes / eligibleNodes : 1;
316
- stats.coverageRatioChars = eligibleChars > 0 ? stats.markedChars / eligibleChars : 1;
317
- }
318
- function scanCriticalTexts() {
319
- const root = document.body;
320
- const viewportHeight = Math.max(0, Math.floor(window.innerHeight || 0));
321
- const viewportWidth = Math.max(0, Math.floor(window.innerWidth || 0));
322
- const viewport = { width: viewportWidth, height: viewportHeight };
323
- if (!root || viewportHeight <= 0)
324
- return { texts: [], viewport };
325
- const seen = new Set();
326
- const texts = [];
327
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
328
- let node = walker.nextNode();
329
- while (node && texts.length < DEFAULT_CRITICAL_MAX) {
330
- if (node.nodeType !== Node.TEXT_NODE) {
331
- node = walker.nextNode();
332
- continue;
333
- }
334
- const textNode = node;
335
- const raw = textNode.nodeValue || "";
336
- const trimmed = raw.trim();
337
- if (!trimmed || !isTranslatableText(trimmed)) {
338
- node = walker.nextNode();
339
- continue;
340
- }
341
- const parent = textNode.parentElement;
342
- if (!parent || isExcludedElement(parent) || findUnsafeContainer(parent)) {
343
- node = walker.nextNode();
344
- continue;
345
- }
346
- const original = getOrInitTextOriginal(textNode, parent);
347
- const originalText = normalizeWhitespace(original.trimmed);
348
- if (!originalText || seen.has(originalText)) {
349
- node = walker.nextNode();
350
- continue;
351
- }
352
- const rect = getTextNodeRect(textNode);
353
- if (!isInViewport(rect, viewportHeight, DEFAULT_CRITICAL_BUFFER_PX)) {
354
- node = walker.nextNode();
355
- continue;
356
- }
357
- seen.add(originalText);
358
- texts.push(originalText);
359
- node = walker.nextNode();
360
- }
361
- if (texts.length < DEFAULT_CRITICAL_MAX) {
362
- const nodes = root.querySelectorAll("[title],[aria-label],[placeholder]");
363
- nodes.forEach((el) => {
364
- if (texts.length >= DEFAULT_CRITICAL_MAX)
365
- return;
366
- if (isExcludedElement(el) || findUnsafeContainer(el))
367
- return;
368
- let rect = null;
369
- try {
370
- rect = el.getBoundingClientRect();
371
- }
372
- catch {
373
- rect = null;
374
- }
375
- if (!isInViewport(rect, viewportHeight, DEFAULT_CRITICAL_BUFFER_PX))
376
- return;
377
- for (const { attr } of ATTRIBUTE_MARKS) {
378
- if (texts.length >= DEFAULT_CRITICAL_MAX)
379
- break;
380
- const value = el.getAttribute(attr);
381
- if (!value)
382
- continue;
383
- const trimmed = value.trim();
384
- if (!trimmed || !isTranslatableText(trimmed))
385
- continue;
386
- const original = normalizeWhitespace(getOrInitAttrOriginal(el, attr));
387
- if (!original || seen.has(original))
388
- continue;
389
- seen.add(original);
390
- texts.push(original);
391
- }
392
- });
393
- }
394
- return { texts, viewport };
395
- }
396
- export function getCriticalFingerprint() {
397
- if (typeof window === "undefined" || typeof document === "undefined") {
398
- return { critical_count: 0, critical_hash: "0", viewport: { width: 0, height: 0 } };
399
- }
400
- const { texts, viewport } = scanCriticalTexts();
401
- const normalized = texts.map((t) => normalizeWhitespace(t)).filter(Boolean);
402
- // Why: sort to stay stable across minor DOM reordering without affecting the set of critical strings.
403
- normalized.sort((a, b) => a.localeCompare(b));
404
- return {
405
- critical_count: normalized.length,
406
- critical_hash: hashContent(normalized.join("\n")),
407
- viewport,
408
- };
409
- }
410
- function scanDom(opts) {
411
- const root = document.body;
412
- if (!root) {
413
- const empty = buildEmptyStats();
414
- setGlobalStats(empty);
415
- return { version: 1, stats: empty, segments: [], occurrences: [], truncated: false };
416
- }
417
- const stats = buildEmptyStats();
418
- const maxSegments = Math.max(0, Math.floor(opts.maxSegments || 0)) || 20000;
419
- const includeCritical = opts.includeCritical === true;
420
- const viewportHeight = includeCritical ? Math.max(0, Math.floor(window.innerHeight || 0)) : 0;
421
- const viewportWidth = includeCritical ? Math.max(0, Math.floor(window.innerWidth || 0)) : 0;
422
- // Why: include a small buffer so "near the fold" text is ready without delaying first paint.
423
- const critical = includeCritical
424
- ? {
425
- enabled: true,
426
- viewportHeight,
427
- bufferPx: DEFAULT_CRITICAL_BUFFER_PX,
428
- max: DEFAULT_CRITICAL_MAX,
429
- seen: new Set(),
430
- occurrences: [],
431
- }
432
- : null;
433
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
434
- const nodes = [];
435
- const segments = [];
436
- const occurrences = [];
437
- const seen = new Set();
438
- let node = walker.nextNode();
439
- while (node) {
440
- if (node.nodeType === Node.TEXT_NODE)
441
- nodes.push(node);
442
- node = walker.nextNode();
443
- }
444
- nodes.forEach((textNode) => {
445
- if (critical?.enabled && critical.occurrences.length >= critical.max) {
446
- critical.enabled = false;
447
- }
448
- considerTextNode(textNode, stats, segments, occurrences, seen, maxSegments, critical);
449
- });
450
- considerAttributes(root, segments, occurrences, seen, maxSegments, critical);
451
- finalizeStats(stats);
452
- setGlobalStats(stats);
453
- const truncated = segments.length >= maxSegments;
454
- return {
455
- version: 1,
456
- stats,
457
- segments,
458
- occurrences,
459
- ...(includeCritical
460
- ? {
461
- critical_occurrences: critical?.occurrences ?? [],
462
- viewport: { width: viewportWidth, height: viewportHeight },
463
- }
464
- : {}),
465
- truncated,
466
- };
467
- }
468
37
  function scheduleScan() {
469
38
  if (!running)
470
39
  return;
@@ -473,8 +42,8 @@ function scheduleScan() {
473
42
  scheduled = window.setTimeout(() => {
474
43
  scheduled = null;
475
44
  try {
476
- scanDom({ maxSegments: 20000 });
477
- if (activeTranslationMap) {
45
+ scanDomWithGlobals({ maxSegments: 20000 });
46
+ if (getActiveTranslationMap()) {
478
47
  applying = true;
479
48
  applyActiveTranslations(document.body);
480
49
  }
@@ -508,7 +77,7 @@ export function startMarkerEngine(options = {}) {
508
77
  subtree: true,
509
78
  characterData: true,
510
79
  });
511
- scanDom({ maxSegments: 20000 });
80
+ scanDomWithGlobals({ maxSegments: 20000 });
512
81
  };
513
82
  startObserver();
514
83
  return stopMarkerEngine;
@@ -529,265 +98,12 @@ export function getMarkerStats() {
529
98
  }
530
99
  export function setMarkerEngineExclusions(exclusions) {
531
100
  if (!exclusions || exclusions.length === 0) {
532
- customExcludeSelector = null;
101
+ setCustomExcludeSelector(null);
533
102
  return;
534
103
  }
535
104
  const selectors = exclusions
536
105
  .filter((e) => e && e.type === "css" && typeof e.selector === "string" && e.selector.trim())
537
106
  .map((e) => e.selector.trim());
538
- customExcludeSelector = selectors.length ? selectors.join(",") : null;
539
- }
540
- export function setActiveTranslations(translations) {
541
- if (!translations || translations.length === 0) {
542
- activeTranslationMap = null;
543
- return;
544
- }
545
- const map = new Map();
546
- for (const t of translations) {
547
- const source = normalizeWhitespace((t?.source_text || "").toString());
548
- const translated = (t?.translated_text ?? "").toString();
549
- if (!source || !translated)
550
- continue;
551
- map.set(source, translated);
552
- }
553
- activeTranslationMap = map;
554
- }
555
- export function addActiveTranslations(translations) {
556
- if (!translations)
557
- return 0;
558
- const map = activeTranslationMap ?? new Map();
559
- let added = 0;
560
- if (Array.isArray(translations)) {
561
- for (const t of translations) {
562
- const source = normalizeWhitespace((t?.source_text || "").toString());
563
- const translated = (t?.translated_text ?? "").toString();
564
- if (!source || !translated)
565
- continue;
566
- if (map.get(source) === translated)
567
- continue;
568
- map.set(source, translated);
569
- added += 1;
570
- }
571
- }
572
- else {
573
- for (const [keyRaw, valueRaw] of Object.entries(translations || {})) {
574
- const source = normalizeWhitespace((keyRaw || "").toString());
575
- const translated = (valueRaw ?? "").toString();
576
- if (!source || !translated)
577
- continue;
578
- if (map.get(source) === translated)
579
- continue;
580
- map.set(source, translated);
581
- added += 1;
582
- }
583
- }
584
- activeTranslationMap = map;
585
- return added;
586
- }
587
- function applyTranslationMap(bundle, root) {
588
- if (!root)
589
- return 0;
590
- const map = new Map();
591
- for (const [k, v] of Object.entries(bundle || {})) {
592
- const source = normalizeWhitespace((k || "").toString());
593
- const translated = (v ?? "").toString();
594
- if (!source || !translated)
595
- continue;
596
- map.set(source, translated);
597
- }
598
- activeTranslationMap = map;
599
- return applyActiveTranslations(root);
600
- }
601
- export function applyActiveTranslations(root = document.body) {
602
- if (!root || !activeTranslationMap || activeTranslationMap.size === 0)
603
- return 0;
604
- const map = activeTranslationMap;
605
- let applied = 0;
606
- const walk = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
607
- const nodes = [];
608
- let node = walk.nextNode();
609
- while (node) {
610
- if (node.nodeType === Node.TEXT_NODE)
611
- nodes.push(node);
612
- node = walk.nextNode();
613
- }
614
- for (const textNode of nodes) {
615
- const parent = textNode.parentElement;
616
- if (!parent)
617
- continue;
618
- const raw = textNode.nodeValue || "";
619
- const trimmed = raw.trim();
620
- if (!trimmed)
621
- continue;
622
- if (isExcludedElement(parent))
623
- continue;
624
- if (findUnsafeContainer(parent))
625
- continue;
626
- if (!isTranslatableText(trimmed))
627
- continue;
628
- const original = getOrInitTextOriginal(textNode, parent);
629
- const key = normalizeWhitespace(original.trimmed);
630
- const translation = map.get(key);
631
- if (!translation)
632
- continue;
633
- const next = `${original.leading}${translation}${original.trailing}`;
634
- if (textNode.nodeValue === next)
635
- continue;
636
- try {
637
- textNode.nodeValue = next;
638
- applied += 1;
639
- }
640
- catch {
641
- // ignore
642
- }
643
- }
644
- if (root instanceof HTMLElement) {
645
- const elements = root.querySelectorAll("[title],[aria-label],[placeholder]");
646
- elements.forEach((el) => {
647
- if (isExcludedElement(el))
648
- return;
649
- if (findUnsafeContainer(el))
650
- return;
651
- for (const { attr } of ATTRIBUTE_MARKS) {
652
- const current = el.getAttribute(attr);
653
- if (!current)
654
- continue;
655
- const trimmed = current.trim();
656
- if (!trimmed || !isTranslatableText(trimmed))
657
- continue;
658
- const original = normalizeWhitespace(getOrInitAttrOriginal(el, attr));
659
- const translation = map.get(original);
660
- if (!translation)
661
- continue;
662
- if (el.getAttribute(attr) === translation)
663
- continue;
664
- try {
665
- el.setAttribute(attr, translation);
666
- applied += 1;
667
- }
668
- catch {
669
- // ignore
670
- }
671
- }
672
- });
673
- }
674
- return applied;
675
- }
676
- export function scanDomForMisses(opts) {
677
- const root = document.body;
678
- const misses = [];
679
- if (!root) {
680
- return { misses };
681
- }
682
- // Why: allow live miss scans even when bundles are empty so first-time pages still report.
683
- const translationMap = activeTranslationMap;
684
- const hasTranslations = Boolean(translationMap && translationMap.size > 0);
685
- const max = Math.max(0, Math.floor(opts.max || 0));
686
- if (max <= 0)
687
- return { misses };
688
- const ignore = opts.ignore || new Set();
689
- const seen = new Set();
690
- const recordMiss = (text, context) => {
691
- if (!text || seen.has(text) || ignore.has(text))
692
- return;
693
- if (hasTranslations && translationMap.has(text))
694
- return;
695
- if (misses.length >= max)
696
- return;
697
- seen.add(text);
698
- misses.push({ source_text: text, semantic_context: context });
699
- };
700
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
701
- let node = walker.nextNode();
702
- while (node && misses.length < max) {
703
- if (node.nodeType !== Node.TEXT_NODE) {
704
- node = walker.nextNode();
705
- continue;
706
- }
707
- const textNode = node;
708
- const parent = textNode.parentElement;
709
- if (!parent || isExcludedElement(parent) || findUnsafeContainer(parent)) {
710
- node = walker.nextNode();
711
- continue;
712
- }
713
- const raw = textNode.nodeValue || "";
714
- const trimmed = raw.trim();
715
- if (!trimmed || !isTranslatableText(trimmed)) {
716
- node = walker.nextNode();
717
- continue;
718
- }
719
- const original = getOrInitTextOriginal(textNode, parent);
720
- const key = normalizeWhitespace(original.trimmed);
721
- if (key) {
722
- recordMiss(key, "text");
723
- }
724
- node = walker.nextNode();
725
- }
726
- if (misses.length < max) {
727
- const nodes = root.querySelectorAll("[title],[aria-label],[placeholder]");
728
- nodes.forEach((el) => {
729
- if (misses.length >= max)
730
- return;
731
- if (isExcludedElement(el) || findUnsafeContainer(el))
732
- return;
733
- for (const { attr } of ATTRIBUTE_MARKS) {
734
- if (misses.length >= max)
735
- break;
736
- const value = el.getAttribute(attr);
737
- if (!value)
738
- continue;
739
- const trimmed = value.trim();
740
- if (!trimmed || !isTranslatableText(trimmed))
741
- continue;
742
- const original = normalizeWhitespace(getOrInitAttrOriginal(el, attr));
743
- if (!original)
744
- continue;
745
- const context = attr === "title" ? "attr:title" : attr === "aria-label" ? "attr:aria-label" : "attr:placeholder";
746
- recordMiss(original, context);
747
- }
748
- });
749
- }
750
- return { misses };
751
- }
752
- export function restoreDom(root = document.body) {
753
- if (!root)
754
- return;
755
- const walk = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
756
- let node = walk.nextNode();
757
- while (node) {
758
- if (node.nodeType === Node.TEXT_NODE) {
759
- const textNode = node;
760
- const original = originalTextByNode.get(textNode);
761
- if (original && textNode.nodeValue !== original.raw) {
762
- try {
763
- textNode.nodeValue = original.raw;
764
- }
765
- catch {
766
- // ignore
767
- }
768
- }
769
- }
770
- node = walk.nextNode();
771
- }
772
- if (root instanceof HTMLElement) {
773
- const elements = root.querySelectorAll("[title],[aria-label],[placeholder]");
774
- elements.forEach((el) => {
775
- const originals = originalAttrByEl.get(el);
776
- if (!originals)
777
- return;
778
- for (const { attr } of ATTRIBUTE_MARKS) {
779
- const original = originals.get(attr);
780
- if (original == null)
781
- continue;
782
- if (el.getAttribute(attr) === original)
783
- continue;
784
- try {
785
- el.setAttribute(attr, original);
786
- }
787
- catch {
788
- // ignore
789
- }
790
- }
791
- });
792
- }
107
+ setCustomExcludeSelector(selectors.length ? selectors.join(",") : null);
793
108
  }
109
+ export { applyActiveTranslations, scanDomForMisses, restoreDom, setActiveTranslations, addActiveTranslations, getCriticalFingerprint };