@lovalingo/lovalingo 0.5.28 → 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 (148) hide show
  1. package/README.md +36 -0
  2. package/dist/chunk-2FZR2AKF.mjs +88 -0
  3. package/dist/chunk-7D5LBV45.mjs +46 -0
  4. package/dist/chunk-CJOSN7RA.mjs +90 -0
  5. package/dist/chunk-VAHA2TOX.mjs +3440 -0
  6. package/dist/chunk-ZMRCSUM7.mjs +26 -0
  7. package/dist/chunk-ZVYKEEUF.mjs +220 -0
  8. package/dist/core.d.mts +131 -0
  9. package/dist/core.d.ts +131 -0
  10. package/dist/core.js +3561 -0
  11. package/dist/core.mjs +19 -0
  12. package/dist/index.d.mts +5 -0
  13. package/dist/index.d.ts +5 -25
  14. package/dist/index.js +3885 -28
  15. package/dist/index.mjs +33 -0
  16. package/dist/react-router.d.mts +101 -0
  17. package/dist/react-router.d.ts +101 -0
  18. package/dist/react-router.js +353 -0
  19. package/dist/react-router.mjs +14 -0
  20. package/dist/tanstack-router.d.mts +22 -0
  21. package/dist/tanstack-router.d.ts +22 -0
  22. package/dist/tanstack-router.js +162 -0
  23. package/dist/tanstack-router.mjs +8 -0
  24. package/package.json +34 -3
  25. package/dist/__tests__/languageFlags.test.d.ts +0 -1
  26. package/dist/__tests__/languageFlags.test.js +0 -42
  27. package/dist/__tests__/mergeEntitlements.test.d.ts +0 -1
  28. package/dist/__tests__/mergeEntitlements.test.js +0 -27
  29. package/dist/components/AixsterProvider.d.ts +0 -1
  30. package/dist/components/AixsterProvider.js +0 -1
  31. package/dist/components/LangLink.d.ts +0 -20
  32. package/dist/components/LangLink.js +0 -38
  33. package/dist/components/LangRouter.d.ts +0 -37
  34. package/dist/components/LangRouter.js +0 -191
  35. package/dist/components/LanguageSwitcher.d.ts +0 -17
  36. package/dist/components/LanguageSwitcher.js +0 -257
  37. package/dist/components/LovalingoProvider.d.ts +0 -10
  38. package/dist/components/LovalingoProvider.js +0 -413
  39. package/dist/components/NavigationOverlay.d.ts +0 -6
  40. package/dist/components/NavigationOverlay.js +0 -22
  41. package/dist/components/provider/__tests__/seoUtils.test.d.ts +0 -1
  42. package/dist/components/provider/__tests__/seoUtils.test.js +0 -13
  43. package/dist/components/provider/editModeUtils.d.ts +0 -6
  44. package/dist/components/provider/editModeUtils.js +0 -59
  45. package/dist/components/provider/localeUtils.d.ts +0 -8
  46. package/dist/components/provider/localeUtils.js +0 -46
  47. package/dist/components/provider/providerConstants.d.ts +0 -12
  48. package/dist/components/provider/providerConstants.js +0 -11
  49. package/dist/components/provider/seoUtils.d.ts +0 -8
  50. package/dist/components/provider/seoUtils.js +0 -118
  51. package/dist/components/provider/useEditModeOverlay.d.ts +0 -7
  52. package/dist/components/provider/useEditModeOverlay.js +0 -134
  53. package/dist/components/provider/useHistoryNavigationPatch.d.ts +0 -3
  54. package/dist/components/provider/useHistoryNavigationPatch.js +0 -47
  55. package/dist/components/provider/useProviderCache.d.ts +0 -12
  56. package/dist/components/provider/useProviderCache.js +0 -82
  57. package/dist/context/AixsterContext.d.ts +0 -3
  58. package/dist/context/AixsterContext.js +0 -2
  59. package/dist/context/LangContext.d.ts +0 -1
  60. package/dist/context/LangContext.js +0 -2
  61. package/dist/context/LangRoutingContext.d.ts +0 -8
  62. package/dist/context/LangRoutingContext.js +0 -7
  63. package/dist/context/LovalingoContext.d.ts +0 -1
  64. package/dist/context/LovalingoContext.js +0 -1
  65. package/dist/hooks/provider/useBundleLoading.d.ts +0 -33
  66. package/dist/hooks/provider/useBundleLoading.js +0 -380
  67. package/dist/hooks/provider/useDomRules.d.ts +0 -15
  68. package/dist/hooks/provider/useDomRules.js +0 -38
  69. package/dist/hooks/provider/useLinkAutoPrefix.d.ts +0 -12
  70. package/dist/hooks/provider/useLinkAutoPrefix.js +0 -146
  71. package/dist/hooks/provider/useNavigationPrefetch.d.ts +0 -12
  72. package/dist/hooks/provider/useNavigationPrefetch.js +0 -82
  73. package/dist/hooks/provider/usePageviewTracking.d.ts +0 -10
  74. package/dist/hooks/provider/usePageviewTracking.js +0 -44
  75. package/dist/hooks/provider/usePrehide.d.ts +0 -5
  76. package/dist/hooks/provider/usePrehide.js +0 -72
  77. package/dist/hooks/provider/useSitemapLinkTag.d.ts +0 -7
  78. package/dist/hooks/provider/useSitemapLinkTag.js +0 -28
  79. package/dist/hooks/provider/useStringMissReporting.d.ts +0 -14
  80. package/dist/hooks/provider/useStringMissReporting.js +0 -155
  81. package/dist/hooks/useAixster.d.ts +0 -6
  82. package/dist/hooks/useAixster.js +0 -14
  83. package/dist/hooks/useAixsterEdit.d.ts +0 -5
  84. package/dist/hooks/useAixsterEdit.js +0 -13
  85. package/dist/hooks/useAixsterTranslate.d.ts +0 -4
  86. package/dist/hooks/useAixsterTranslate.js +0 -12
  87. package/dist/hooks/useLang.d.ts +0 -16
  88. package/dist/hooks/useLang.js +0 -23
  89. package/dist/hooks/useLangNavigate.d.ts +0 -24
  90. package/dist/hooks/useLangNavigate.js +0 -40
  91. package/dist/hooks/useLovalingo.d.ts +0 -1
  92. package/dist/hooks/useLovalingo.js +0 -1
  93. package/dist/hooks/useLovalingoEdit.d.ts +0 -1
  94. package/dist/hooks/useLovalingoEdit.js +0 -1
  95. package/dist/hooks/useLovalingoTranslate.d.ts +0 -1
  96. package/dist/hooks/useLovalingoTranslate.js +0 -1
  97. package/dist/types.d.ts +0 -76
  98. package/dist/types.js +0 -1
  99. package/dist/utils/api.d.ts +0 -42
  100. package/dist/utils/api.js +0 -395
  101. package/dist/utils/apiTypes.d.ts +0 -78
  102. package/dist/utils/apiTypes.js +0 -1
  103. package/dist/utils/apiUtils.d.ts +0 -4
  104. package/dist/utils/apiUtils.js +0 -54
  105. package/dist/utils/domRules.d.ts +0 -2
  106. package/dist/utils/domRules.js +0 -150
  107. package/dist/utils/hash.d.ts +0 -9
  108. package/dist/utils/hash.js +0 -27
  109. package/dist/utils/languageFlags.d.ts +0 -7
  110. package/dist/utils/languageFlags.js +0 -90
  111. package/dist/utils/logger.d.ts +0 -3
  112. package/dist/utils/logger.js +0 -40
  113. package/dist/utils/markerEngine.d.ts +0 -12
  114. package/dist/utils/markerEngine.js +0 -109
  115. package/dist/utils/markerEngineApply.d.ts +0 -3
  116. package/dist/utils/markerEngineApply.js +0 -136
  117. package/dist/utils/markerEngineConstants.d.ts +0 -10
  118. package/dist/utils/markerEngineConstants.js +0 -12
  119. package/dist/utils/markerEngineCritical.d.ts +0 -2
  120. package/dist/utils/markerEngineCritical.js +0 -98
  121. package/dist/utils/markerEngineDomUtils.d.ts +0 -8
  122. package/dist/utils/markerEngineDomUtils.js +0 -74
  123. package/dist/utils/markerEngineFilters.d.ts +0 -2
  124. package/dist/utils/markerEngineFilters.js +0 -26
  125. package/dist/utils/markerEngineMisses.d.ts +0 -5
  126. package/dist/utils/markerEngineMisses.js +0 -81
  127. package/dist/utils/markerEngineOriginals.d.ts +0 -5
  128. package/dist/utils/markerEngineOriginals.js +0 -29
  129. package/dist/utils/markerEngineScan.d.ts +0 -5
  130. package/dist/utils/markerEngineScan.js +0 -162
  131. package/dist/utils/markerEngineState.d.ts +0 -4
  132. package/dist/utils/markerEngineState.js +0 -14
  133. package/dist/utils/markerEngineStats.d.ts +0 -3
  134. package/dist/utils/markerEngineStats.js +0 -28
  135. package/dist/utils/markerEngineTranslations.d.ts +0 -3
  136. package/dist/utils/markerEngineTranslations.js +0 -49
  137. package/dist/utils/markerEngineTypes.d.ts +0 -62
  138. package/dist/utils/markerEngineTypes.js +0 -1
  139. package/dist/utils/markerEngineViewport.d.ts +0 -2
  140. package/dist/utils/markerEngineViewport.js +0 -27
  141. package/dist/utils/mergeEntitlements.d.ts +0 -2
  142. package/dist/utils/mergeEntitlements.js +0 -7
  143. package/dist/utils/nonLocalizedPaths.d.ts +0 -12
  144. package/dist/utils/nonLocalizedPaths.js +0 -136
  145. package/dist/utils/pathNormalizer.d.ts +0 -49
  146. package/dist/utils/pathNormalizer.js +0 -115
  147. package/dist/version.d.ts +0 -1
  148. package/dist/version.js +0 -1
@@ -1,98 +0,0 @@
1
- import { hashContent } from "./hash";
2
- import { ATTRIBUTE_MARKS, DEFAULT_CRITICAL_BUFFER_PX, DEFAULT_CRITICAL_MAX } from "./markerEngineConstants";
3
- import { isExcludedElement, findUnsafeContainer } from "./markerEngineFilters";
4
- import { isTranslatableText, normalizeWhitespace } from "./markerEngineDomUtils";
5
- import { getOrInitAttrOriginal, getOrInitTextOriginal } from "./markerEngineOriginals";
6
- import { getTextNodeRect, isInViewport } from "./markerEngineViewport";
7
- function scanCriticalTexts() {
8
- const root = document.body;
9
- const viewportHeight = Math.max(0, Math.floor(window.innerHeight || 0));
10
- const viewportWidth = Math.max(0, Math.floor(window.innerWidth || 0));
11
- const viewport = { width: viewportWidth, height: viewportHeight };
12
- if (!root || viewportHeight <= 0)
13
- return { texts: [], viewport };
14
- const seen = new Set();
15
- const texts = [];
16
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
17
- let node = walker.nextNode();
18
- while (node && texts.length < DEFAULT_CRITICAL_MAX) {
19
- if (node.nodeType !== Node.TEXT_NODE) {
20
- node = walker.nextNode();
21
- continue;
22
- }
23
- const textNode = node;
24
- const raw = textNode.nodeValue || "";
25
- const trimmed = raw.trim();
26
- if (!trimmed || !isTranslatableText(trimmed)) {
27
- node = walker.nextNode();
28
- continue;
29
- }
30
- const parent = textNode.parentElement;
31
- if (!parent || isExcludedElement(parent) || findUnsafeContainer(parent)) {
32
- node = walker.nextNode();
33
- continue;
34
- }
35
- const original = getOrInitTextOriginal(textNode, parent);
36
- const originalText = normalizeWhitespace(original.trimmed);
37
- if (!originalText || seen.has(originalText)) {
38
- node = walker.nextNode();
39
- continue;
40
- }
41
- const rect = getTextNodeRect(textNode);
42
- if (!isInViewport(rect, viewportHeight, DEFAULT_CRITICAL_BUFFER_PX)) {
43
- node = walker.nextNode();
44
- continue;
45
- }
46
- seen.add(originalText);
47
- texts.push(originalText);
48
- node = walker.nextNode();
49
- }
50
- if (texts.length < DEFAULT_CRITICAL_MAX) {
51
- const nodes = root.querySelectorAll("[title],[aria-label],[placeholder]");
52
- nodes.forEach((el) => {
53
- if (texts.length >= DEFAULT_CRITICAL_MAX)
54
- return;
55
- if (isExcludedElement(el) || findUnsafeContainer(el))
56
- return;
57
- let rect = null;
58
- try {
59
- rect = el.getBoundingClientRect();
60
- }
61
- catch {
62
- rect = null;
63
- }
64
- if (!isInViewport(rect, viewportHeight, DEFAULT_CRITICAL_BUFFER_PX))
65
- return;
66
- for (const { attr } of ATTRIBUTE_MARKS) {
67
- if (texts.length >= DEFAULT_CRITICAL_MAX)
68
- break;
69
- const value = el.getAttribute(attr);
70
- if (!value)
71
- continue;
72
- const trimmed = value.trim();
73
- if (!trimmed || !isTranslatableText(trimmed))
74
- continue;
75
- const original = normalizeWhitespace(getOrInitAttrOriginal(el, attr));
76
- if (!original || seen.has(original))
77
- continue;
78
- seen.add(original);
79
- texts.push(original);
80
- }
81
- });
82
- }
83
- return { texts, viewport };
84
- }
85
- export function getCriticalFingerprint() {
86
- if (typeof window === "undefined" || typeof document === "undefined") {
87
- return { critical_count: 0, critical_hash: "0", viewport: { width: 0, height: 0 } };
88
- }
89
- const { texts, viewport } = scanCriticalTexts();
90
- const normalized = texts.map((t) => normalizeWhitespace(t)).filter(Boolean);
91
- // Why: sort to stay stable across minor DOM reordering without affecting the set of critical strings.
92
- normalized.sort((a, b) => a.localeCompare(b));
93
- return {
94
- critical_count: normalized.length,
95
- critical_hash: hashContent(normalized.join("\n")),
96
- viewport,
97
- };
98
- }
@@ -1,8 +0,0 @@
1
- export declare function getStableKey(el: Element): string;
2
- export declare function getElementIndex(el: Element): number;
3
- export declare function getTextNodeIndex(node: Text): number;
4
- export declare function buildElementPath(el: Element): string;
5
- export declare function normalizeWhitespace(value: string): string;
6
- export declare function isTranslatableText(text: string): boolean;
7
- export declare function buildStableId(el: Element, text: string, textIndex: number): string;
8
- export declare function buildSelector(el: Element): string | null;
@@ -1,74 +0,0 @@
1
- import { hashContent } from "./hash";
2
- export function getStableKey(el) {
3
- const owner = el.closest("[data-lovalingo-key]");
4
- const key = owner?.getAttribute("data-lovalingo-key") || "";
5
- return key.trim();
6
- }
7
- export function getElementIndex(el) {
8
- const parent = el.parentElement;
9
- if (!parent)
10
- return 0;
11
- const children = Array.from(parent.children);
12
- const idx = children.indexOf(el);
13
- return idx >= 0 ? idx : 0;
14
- }
15
- export function getTextNodeIndex(node) {
16
- let index = 0;
17
- let prev = node.previousSibling;
18
- while (prev) {
19
- if (prev.nodeType === Node.TEXT_NODE)
20
- index += 1;
21
- prev = prev.previousSibling;
22
- }
23
- return index;
24
- }
25
- export function buildElementPath(el) {
26
- const parts = [];
27
- let current = el;
28
- while (current && current.tagName && current !== document.body) {
29
- const tag = current.tagName.toLowerCase();
30
- const idx = getElementIndex(current);
31
- parts.push(`${tag}[${idx}]`);
32
- current = current.parentElement;
33
- }
34
- parts.push("body");
35
- return parts.reverse().join("/");
36
- }
37
- export function normalizeWhitespace(value) {
38
- return (value || "").toString().replace(/\s+/g, " ").trim();
39
- }
40
- export function isTranslatableText(text) {
41
- if (!text || text.trim().length < 2)
42
- return false;
43
- if (/^(__[A-Z0-9_]+__\s*)+$/.test(text))
44
- return false;
45
- if (/^\d+(\.\d+)?$/.test(text))
46
- return false;
47
- if (!/[a-zA-Z\u00C0-\u024F\u1E00-\u1EFF]/.test(text))
48
- return false;
49
- return true;
50
- }
51
- export function buildStableId(el, text, textIndex) {
52
- const key = getStableKey(el);
53
- const path = buildElementPath(el);
54
- const raw = `${path}#text[${textIndex}]|${text.trim()}|${key}`;
55
- return hashContent(raw);
56
- }
57
- export function buildSelector(el) {
58
- const id = el.id;
59
- if (id)
60
- return `#${id.replace(/[^a-zA-Z0-9_-]/g, "\\$&")}`;
61
- const className = el.className;
62
- if (typeof className === "string" && className.trim()) {
63
- const classes = className
64
- .split(/\s+/)
65
- .map((c) => c.trim())
66
- .filter(Boolean)
67
- .slice(0, 3)
68
- .map((c) => `.${c.replace(/[^a-zA-Z0-9_-]/g, "\\$&")}`)
69
- .join("");
70
- if (classes)
71
- return classes;
72
- }
73
- return null;
74
- }
@@ -1,2 +0,0 @@
1
- export declare function isExcludedElement(el: Element | null): boolean;
2
- export declare function findUnsafeContainer(el: Element | null): Element | null;
@@ -1,26 +0,0 @@
1
- import { EXCLUDE_SELECTOR, unsafeSelector } from "./markerEngineConstants";
2
- import { getCustomExcludeSelector } from "./markerEngineState";
3
- export function isExcludedElement(el) {
4
- if (!el)
5
- return false;
6
- if (el.closest(EXCLUDE_SELECTOR))
7
- return true;
8
- const customExcludeSelector = getCustomExcludeSelector();
9
- if (customExcludeSelector) {
10
- try {
11
- if (el.closest(customExcludeSelector))
12
- return true;
13
- }
14
- catch {
15
- // ignore invalid selector strings
16
- }
17
- }
18
- return false;
19
- }
20
- export function findUnsafeContainer(el) {
21
- if (!el)
22
- return null;
23
- if (!unsafeSelector)
24
- return null;
25
- return el.closest(unsafeSelector);
26
- }
@@ -1,5 +0,0 @@
1
- import type { DomMissScanResult } from "./markerEngineTypes";
2
- export declare function scanDomForMisses(opts: {
3
- max: number;
4
- ignore?: Set<string>;
5
- }): DomMissScanResult;
@@ -1,81 +0,0 @@
1
- import { ATTRIBUTE_MARKS } from "./markerEngineConstants";
2
- import { findUnsafeContainer, isExcludedElement } from "./markerEngineFilters";
3
- import { getActiveTranslationMap } from "./markerEngineState";
4
- import { isTranslatableText, normalizeWhitespace } from "./markerEngineDomUtils";
5
- import { getOrInitAttrOriginal, getOrInitTextOriginal } from "./markerEngineOriginals";
6
- export function scanDomForMisses(opts) {
7
- const root = document.body;
8
- const misses = [];
9
- if (!root) {
10
- return { misses };
11
- }
12
- // Why: allow live miss scans even when bundles are empty so first-time pages still report.
13
- const translationMap = getActiveTranslationMap();
14
- const hasTranslations = Boolean(translationMap && translationMap.size > 0);
15
- const max = Math.max(0, Math.floor(opts.max || 0));
16
- if (max <= 0)
17
- return { misses };
18
- const ignore = opts.ignore || new Set();
19
- const seen = new Set();
20
- const recordMiss = (text, context) => {
21
- if (!text || seen.has(text) || ignore.has(text))
22
- return;
23
- if (hasTranslations && translationMap.has(text))
24
- return;
25
- if (misses.length >= max)
26
- return;
27
- seen.add(text);
28
- misses.push({ source_text: text, semantic_context: context });
29
- };
30
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
31
- let node = walker.nextNode();
32
- while (node && misses.length < max) {
33
- if (node.nodeType !== Node.TEXT_NODE) {
34
- node = walker.nextNode();
35
- continue;
36
- }
37
- const textNode = node;
38
- const parent = textNode.parentElement;
39
- if (!parent || isExcludedElement(parent) || findUnsafeContainer(parent)) {
40
- node = walker.nextNode();
41
- continue;
42
- }
43
- const raw = textNode.nodeValue || "";
44
- const trimmed = raw.trim();
45
- if (!trimmed || !isTranslatableText(trimmed)) {
46
- node = walker.nextNode();
47
- continue;
48
- }
49
- const original = getOrInitTextOriginal(textNode, parent);
50
- const key = normalizeWhitespace(original.trimmed);
51
- if (key) {
52
- recordMiss(key, "text");
53
- }
54
- node = walker.nextNode();
55
- }
56
- if (misses.length < max) {
57
- const nodes = root.querySelectorAll("[title],[aria-label],[placeholder]");
58
- nodes.forEach((el) => {
59
- if (misses.length >= max)
60
- return;
61
- if (isExcludedElement(el) || findUnsafeContainer(el))
62
- return;
63
- for (const { attr } of ATTRIBUTE_MARKS) {
64
- if (misses.length >= max)
65
- break;
66
- const value = el.getAttribute(attr);
67
- if (!value)
68
- continue;
69
- const trimmed = value.trim();
70
- if (!trimmed || !isTranslatableText(trimmed))
71
- continue;
72
- const original = normalizeWhitespace(getOrInitAttrOriginal(el, attr));
73
- if (!original)
74
- continue;
75
- const context = attr === "title" ? "attr:title" : attr === "aria-label" ? "attr:aria-label" : "attr:placeholder";
76
- recordMiss(original, context);
77
- }
78
- });
79
- }
80
- return { misses };
81
- }
@@ -1,5 +0,0 @@
1
- import type { TextOriginal } from "./markerEngineTypes";
2
- export declare const originalTextByNode: WeakMap<Text, TextOriginal>;
3
- export declare const originalAttrByEl: WeakMap<HTMLElement, Map<string, string>>;
4
- export declare function getOrInitTextOriginal(node: Text, parent: Element): TextOriginal;
5
- export declare function getOrInitAttrOriginal(el: HTMLElement, attr: string): string;
@@ -1,29 +0,0 @@
1
- import { buildStableId, getTextNodeIndex } from "./markerEngineDomUtils";
2
- export const originalTextByNode = new WeakMap();
3
- export const originalAttrByEl = new WeakMap();
4
- export function getOrInitTextOriginal(node, parent) {
5
- const existing = originalTextByNode.get(node);
6
- if (existing)
7
- return existing;
8
- const raw = node.nodeValue || "";
9
- const leading = raw.match(/^\s*/)?.[0] ?? "";
10
- const trailing = raw.match(/\s*$/)?.[0] ?? "";
11
- const trimmed = raw.trim();
12
- const id = buildStableId(parent, trimmed, getTextNodeIndex(node));
13
- const created = { raw, trimmed, leading, trailing, id };
14
- originalTextByNode.set(node, created);
15
- return created;
16
- }
17
- export function getOrInitAttrOriginal(el, attr) {
18
- let map = originalAttrByEl.get(el);
19
- if (!map) {
20
- map = new Map();
21
- originalAttrByEl.set(el, map);
22
- }
23
- const existing = map.get(attr);
24
- if (existing != null)
25
- return existing;
26
- const value = (el.getAttribute(attr) || "").toString();
27
- map.set(attr, value);
28
- return value;
29
- }
@@ -1,5 +0,0 @@
1
- import type { DomScanResult } from "./markerEngineTypes";
2
- export declare function scanDom(opts: {
3
- maxSegments: number;
4
- includeCritical?: boolean;
5
- }): DomScanResult;
@@ -1,162 +0,0 @@
1
- import { ATTRIBUTE_MARKS, DEFAULT_CRITICAL_BUFFER_PX, DEFAULT_CRITICAL_MAX, } from "./markerEngineConstants";
2
- import { isExcludedElement, findUnsafeContainer } from "./markerEngineFilters";
3
- import { buildSelector, isTranslatableText, normalizeWhitespace } from "./markerEngineDomUtils";
4
- import { getOrInitAttrOriginal, getOrInitTextOriginal } from "./markerEngineOriginals";
5
- import { buildEmptyStats, finalizeStats } from "./markerEngineStats";
6
- import { getTextNodeRect, isInViewport } from "./markerEngineViewport";
7
- function considerTextNode(node, stats, segments, occurrences, seen, maxSegments, critical) {
8
- const raw = node.nodeValue || "";
9
- if (!raw)
10
- return;
11
- const trimmed = raw.trim();
12
- if (!trimmed)
13
- return;
14
- stats.totalTextNodes += 1;
15
- stats.totalChars += raw.length;
16
- const parent = node.parentElement;
17
- if (!parent)
18
- return;
19
- if (isExcludedElement(parent)) {
20
- stats.skippedExcludedNodes += 1;
21
- stats.skippedExcludedChars += raw.length;
22
- return;
23
- }
24
- const unsafe = findUnsafeContainer(parent);
25
- if (unsafe) {
26
- stats.skippedUnsafeNodes += 1;
27
- stats.skippedUnsafeChars += raw.length;
28
- return;
29
- }
30
- if (!isTranslatableText(trimmed)) {
31
- stats.skippedNonTranslatableNodes += 1;
32
- stats.skippedNonTranslatableChars += raw.length;
33
- return;
34
- }
35
- const original = getOrInitTextOriginal(node, parent);
36
- stats.markedNodes += 1;
37
- stats.markedChars += raw.length;
38
- if (segments.length < maxSegments) {
39
- const originalText = normalizeWhitespace(original.trimmed) || null;
40
- const currentText = normalizeWhitespace(node.nodeValue || "") || null;
41
- segments.push({
42
- kind: "text",
43
- selector: buildSelector(parent),
44
- original: originalText,
45
- current: currentText,
46
- html: null,
47
- });
48
- if (originalText && !seen.has(originalText)) {
49
- seen.add(originalText);
50
- occurrences.push({ source_text: originalText, semantic_context: "text" });
51
- }
52
- if (critical?.enabled && originalText && !critical.seen.has(originalText)) {
53
- const rect = getTextNodeRect(node);
54
- if (isInViewport(rect, critical.viewportHeight, critical.bufferPx)) {
55
- critical.seen.add(originalText);
56
- critical.occurrences.push({ source_text: originalText, semantic_context: "critical:text" });
57
- }
58
- }
59
- }
60
- }
61
- function considerAttributes(root, segments, occurrences, seen, maxSegments, critical) {
62
- const nodes = root.querySelectorAll("[title],[aria-label],[placeholder]");
63
- nodes.forEach((el) => {
64
- if (isExcludedElement(el))
65
- return;
66
- if (findUnsafeContainer(el))
67
- return;
68
- for (const { attr } of ATTRIBUTE_MARKS) {
69
- const value = el.getAttribute(attr);
70
- if (!value)
71
- continue;
72
- const trimmed = value.trim();
73
- if (!trimmed || !isTranslatableText(trimmed))
74
- continue;
75
- const original = normalizeWhitespace(getOrInitAttrOriginal(el, attr)) || null;
76
- const current = normalizeWhitespace(el.getAttribute(attr) || "") || null;
77
- const kind = (attr === "title" ? "title" : attr === "aria-label" ? "aria-label" : "placeholder");
78
- if (segments.length < maxSegments) {
79
- segments.push({
80
- kind,
81
- selector: buildSelector(el),
82
- original,
83
- current,
84
- html: null,
85
- });
86
- }
87
- if (original && !seen.has(original)) {
88
- seen.add(original);
89
- occurrences.push({ source_text: original, semantic_context: `attr:${attr}` });
90
- }
91
- if (critical?.enabled && original && !critical.seen.has(original)) {
92
- let rect = null;
93
- try {
94
- rect = el.getBoundingClientRect();
95
- }
96
- catch {
97
- rect = null;
98
- }
99
- if (isInViewport(rect, critical.viewportHeight, critical.bufferPx)) {
100
- critical.seen.add(original);
101
- critical.occurrences.push({ source_text: original, semantic_context: `critical:attr:${attr}` });
102
- }
103
- }
104
- }
105
- });
106
- }
107
- export function scanDom(opts) {
108
- const root = document.body;
109
- if (!root) {
110
- const empty = buildEmptyStats();
111
- return { version: 1, stats: empty, segments: [], occurrences: [], truncated: false };
112
- }
113
- const stats = buildEmptyStats();
114
- const maxSegments = Math.max(0, Math.floor(opts.maxSegments || 0)) || 20000;
115
- const includeCritical = opts.includeCritical === true;
116
- const viewportHeight = includeCritical ? Math.max(0, Math.floor(window.innerHeight || 0)) : 0;
117
- const viewportWidth = includeCritical ? Math.max(0, Math.floor(window.innerWidth || 0)) : 0;
118
- // Why: include a small buffer so "near the fold" text is ready without delaying first paint.
119
- const critical = includeCritical
120
- ? {
121
- enabled: true,
122
- viewportHeight,
123
- bufferPx: DEFAULT_CRITICAL_BUFFER_PX,
124
- max: DEFAULT_CRITICAL_MAX,
125
- seen: new Set(),
126
- occurrences: [],
127
- }
128
- : null;
129
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
130
- const nodes = [];
131
- const segments = [];
132
- const occurrences = [];
133
- const seen = new Set();
134
- let node = walker.nextNode();
135
- while (node) {
136
- if (node.nodeType === Node.TEXT_NODE)
137
- nodes.push(node);
138
- node = walker.nextNode();
139
- }
140
- nodes.forEach((textNode) => {
141
- if (critical?.enabled && critical.occurrences.length >= critical.max) {
142
- critical.enabled = false;
143
- }
144
- considerTextNode(textNode, stats, segments, occurrences, seen, maxSegments, critical);
145
- });
146
- considerAttributes(root, segments, occurrences, seen, maxSegments, critical);
147
- finalizeStats(stats);
148
- const truncated = segments.length >= maxSegments;
149
- return {
150
- version: 1,
151
- stats,
152
- segments,
153
- occurrences,
154
- ...(includeCritical
155
- ? {
156
- critical_occurrences: critical?.occurrences ?? [],
157
- viewport: { width: viewportWidth, height: viewportHeight },
158
- }
159
- : {}),
160
- truncated,
161
- };
162
- }
@@ -1,4 +0,0 @@
1
- export declare function getCustomExcludeSelector(): string | null;
2
- export declare function setCustomExcludeSelector(value: string | null): void;
3
- export declare function getActiveTranslationMap(): Map<string, string> | null;
4
- export declare function setActiveTranslationMap(value: Map<string, string> | null): void;
@@ -1,14 +0,0 @@
1
- let customExcludeSelector = null;
2
- let activeTranslationMap = null;
3
- export function getCustomExcludeSelector() {
4
- return customExcludeSelector;
5
- }
6
- export function setCustomExcludeSelector(value) {
7
- customExcludeSelector = value;
8
- }
9
- export function getActiveTranslationMap() {
10
- return activeTranslationMap;
11
- }
12
- export function setActiveTranslationMap(value) {
13
- activeTranslationMap = value;
14
- }
@@ -1,3 +0,0 @@
1
- import type { MarkerStats } from "./markerEngineTypes";
2
- export declare function buildEmptyStats(): MarkerStats;
3
- export declare function finalizeStats(stats: MarkerStats): void;
@@ -1,28 +0,0 @@
1
- export function buildEmptyStats() {
2
- return {
3
- totalTextNodes: 0,
4
- markedNodes: 0,
5
- skippedUnsafeNodes: 0,
6
- skippedExcludedNodes: 0,
7
- skippedNonTranslatableNodes: 0,
8
- totalChars: 0,
9
- markedChars: 0,
10
- skippedUnsafeChars: 0,
11
- skippedExcludedChars: 0,
12
- skippedNonTranslatableChars: 0,
13
- coverageRatio: 0,
14
- coverageRatioChars: 0,
15
- };
16
- }
17
- export function finalizeStats(stats) {
18
- const eligibleNodes = stats.totalTextNodes -
19
- stats.skippedUnsafeNodes -
20
- stats.skippedExcludedNodes -
21
- stats.skippedNonTranslatableNodes;
22
- const eligibleChars = stats.totalChars -
23
- stats.skippedUnsafeChars -
24
- stats.skippedExcludedChars -
25
- stats.skippedNonTranslatableChars;
26
- stats.coverageRatio = eligibleNodes > 0 ? stats.markedNodes / eligibleNodes : 1;
27
- stats.coverageRatioChars = eligibleChars > 0 ? stats.markedChars / eligibleChars : 1;
28
- }
@@ -1,3 +0,0 @@
1
- import type { Translation } from "../types";
2
- export declare function setActiveTranslations(translations: Translation[] | null): void;
3
- export declare function addActiveTranslations(translations: Translation[] | Record<string, string> | null): number;
@@ -1,49 +0,0 @@
1
- import { normalizeWhitespace } from "./markerEngineDomUtils";
2
- import { getActiveTranslationMap, setActiveTranslationMap } from "./markerEngineState";
3
- export function setActiveTranslations(translations) {
4
- if (!translations || translations.length === 0) {
5
- setActiveTranslationMap(null);
6
- return;
7
- }
8
- const map = new Map();
9
- for (const t of translations) {
10
- const source = normalizeWhitespace((t?.source_text || "").toString());
11
- const translated = (t?.translated_text ?? "").toString();
12
- if (!source || !translated)
13
- continue;
14
- map.set(source, translated);
15
- }
16
- setActiveTranslationMap(map);
17
- }
18
- export function addActiveTranslations(translations) {
19
- if (!translations)
20
- return 0;
21
- const map = getActiveTranslationMap() ?? new Map();
22
- let added = 0;
23
- if (Array.isArray(translations)) {
24
- for (const t of translations) {
25
- const source = normalizeWhitespace((t?.source_text || "").toString());
26
- const translated = (t?.translated_text ?? "").toString();
27
- if (!source || !translated)
28
- continue;
29
- if (map.get(source) === translated)
30
- continue;
31
- map.set(source, translated);
32
- added += 1;
33
- }
34
- }
35
- else {
36
- for (const [keyRaw, valueRaw] of Object.entries(translations || {})) {
37
- const source = normalizeWhitespace((keyRaw || "").toString());
38
- const translated = (valueRaw ?? "").toString();
39
- if (!source || !translated)
40
- continue;
41
- if (map.get(source) === translated)
42
- continue;
43
- map.set(source, translated);
44
- added += 1;
45
- }
46
- }
47
- setActiveTranslationMap(map);
48
- return added;
49
- }
@@ -1,62 +0,0 @@
1
- export type MarkerStats = {
2
- totalTextNodes: number;
3
- markedNodes: number;
4
- skippedUnsafeNodes: number;
5
- skippedExcludedNodes: number;
6
- skippedNonTranslatableNodes: number;
7
- totalChars: number;
8
- markedChars: number;
9
- skippedUnsafeChars: number;
10
- skippedExcludedChars: number;
11
- skippedNonTranslatableChars: number;
12
- coverageRatio: number;
13
- coverageRatioChars: number;
14
- };
15
- export type MarkerEngineOptions = {
16
- throttleMs?: number;
17
- };
18
- export type DomScanOccurrence = {
19
- source_text: string;
20
- semantic_context?: string;
21
- };
22
- export type DomScanSegment = {
23
- kind: "text" | "title" | "aria-label" | "placeholder";
24
- selector: string | null;
25
- original: string | null;
26
- current: string | null;
27
- html: null;
28
- };
29
- export type DomScanResult = {
30
- version: 1;
31
- stats: MarkerStats;
32
- segments: DomScanSegment[];
33
- occurrences: DomScanOccurrence[];
34
- critical_occurrences?: DomScanOccurrence[];
35
- viewport?: {
36
- width: number;
37
- height: number;
38
- };
39
- truncated: boolean;
40
- };
41
- export type DomMiss = {
42
- source_text: string;
43
- semantic_context: string;
44
- };
45
- export type DomMissScanResult = {
46
- misses: DomMiss[];
47
- };
48
- export type CriticalFingerprint = {
49
- critical_count: number;
50
- critical_hash: string;
51
- viewport: {
52
- width: number;
53
- height: number;
54
- };
55
- };
56
- export type TextOriginal = {
57
- raw: string;
58
- trimmed: string;
59
- leading: string;
60
- trailing: string;
61
- id: string;
62
- };