@lovalingo/lovalingo 0.5.9 → 0.5.10
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.
- package/dist/components/LovalingoProvider.js +12 -0
- package/dist/hooks/provider/useStringMissReporting.d.ts +14 -0
- package/dist/hooks/provider/useStringMissReporting.js +150 -0
- package/dist/utils/api.d.ts +19 -0
- package/dist/utils/api.js +36 -0
- package/dist/utils/markerEngine.d.ts +15 -0
- package/dist/utils/markerEngine.js +153 -0
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ import { useNavigationPrefetch } from '../hooks/provider/useNavigationPrefetch';
|
|
|
11
11
|
import { useLinkAutoPrefix } from '../hooks/provider/useLinkAutoPrefix';
|
|
12
12
|
import { usePageviewTracking } from '../hooks/provider/usePageviewTracking';
|
|
13
13
|
import { useSitemapLinkTag } from '../hooks/provider/useSitemapLinkTag';
|
|
14
|
+
import { useStringMissReporting } from '../hooks/provider/useStringMissReporting';
|
|
14
15
|
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
15
16
|
const LOCALE_STORAGE_KEY = 'Lovalingo_locale';
|
|
16
17
|
const LOADING_BG_STORAGE_PREFIX = "Lovalingo_loading_bg_color";
|
|
@@ -563,6 +564,17 @@ navigateRef, // For path mode routing
|
|
|
563
564
|
allLocales,
|
|
564
565
|
enhancedPathConfig,
|
|
565
566
|
});
|
|
567
|
+
useStringMissReporting({
|
|
568
|
+
apiRef,
|
|
569
|
+
resolvedApiKey,
|
|
570
|
+
locale,
|
|
571
|
+
defaultLocale,
|
|
572
|
+
routing,
|
|
573
|
+
allLocales,
|
|
574
|
+
nonLocalizedPaths: routingConfig.nonLocalizedPaths,
|
|
575
|
+
isLoading,
|
|
576
|
+
mode,
|
|
577
|
+
});
|
|
566
578
|
// Dynamic DOM updates are handled by the marker engine observer (React-safe in-place text/attr updates).
|
|
567
579
|
// No periodic string-miss reporting. Page discovery is tracked via pageview only.
|
|
568
580
|
const translateElement = useCallback((element) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MutableRefObject } from "react";
|
|
2
|
+
import type { LovalingoAPI } from "../../utils/api";
|
|
3
|
+
import { type NonLocalizedPathRule } from "../../utils/nonLocalizedPaths";
|
|
4
|
+
export declare function useStringMissReporting(args: {
|
|
5
|
+
apiRef: MutableRefObject<LovalingoAPI>;
|
|
6
|
+
resolvedApiKey: string;
|
|
7
|
+
locale: string;
|
|
8
|
+
defaultLocale: string;
|
|
9
|
+
routing: "path" | "query";
|
|
10
|
+
allLocales: string[];
|
|
11
|
+
nonLocalizedPaths: NonLocalizedPathRule[];
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
mode: "dom";
|
|
14
|
+
}): void;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { addActiveTranslations, applyActiveTranslations, clearMissBlur, scanDomForMisses } from "../../utils/markerEngine";
|
|
3
|
+
import { isNonLocalizedPath, stripLocalePrefix } from "../../utils/nonLocalizedPaths";
|
|
4
|
+
// Why: throttle miss scans so we stay responsive on DOM-heavy pages.
|
|
5
|
+
const MISS_SCAN_THROTTLE_MS = 600;
|
|
6
|
+
// Why: allow large pages to report plenty of misses while keeping payloads bounded.
|
|
7
|
+
const MISS_MAX_PER_PAGE = 500;
|
|
8
|
+
export function useStringMissReporting(args) {
|
|
9
|
+
const pendingRef = useRef(new Set());
|
|
10
|
+
const seenRef = useRef(new Set());
|
|
11
|
+
const blurredRef = useRef(new Map());
|
|
12
|
+
const scheduledRef = useRef(null);
|
|
13
|
+
const inFlightRef = useRef(false);
|
|
14
|
+
const mergeBlurred = useCallback((incoming) => {
|
|
15
|
+
for (const [text, elements] of incoming.entries()) {
|
|
16
|
+
let set = blurredRef.current.get(text);
|
|
17
|
+
if (!set) {
|
|
18
|
+
set = new Set();
|
|
19
|
+
blurredRef.current.set(text, set);
|
|
20
|
+
}
|
|
21
|
+
elements.forEach((el) => set.add(el));
|
|
22
|
+
}
|
|
23
|
+
}, []);
|
|
24
|
+
const clearBlurForText = useCallback((text) => {
|
|
25
|
+
const elements = blurredRef.current.get(text);
|
|
26
|
+
if (!elements)
|
|
27
|
+
return;
|
|
28
|
+
clearMissBlur(elements);
|
|
29
|
+
blurredRef.current.delete(text);
|
|
30
|
+
}, []);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
pendingRef.current.clear();
|
|
33
|
+
seenRef.current.clear();
|
|
34
|
+
for (const elements of blurredRef.current.values()) {
|
|
35
|
+
clearMissBlur(elements);
|
|
36
|
+
}
|
|
37
|
+
blurredRef.current.clear();
|
|
38
|
+
}, [args.locale]);
|
|
39
|
+
const shouldSkip = useCallback(() => {
|
|
40
|
+
if (typeof window === "undefined" || typeof document === "undefined")
|
|
41
|
+
return true;
|
|
42
|
+
if (!args.resolvedApiKey || args.mode !== "dom")
|
|
43
|
+
return true;
|
|
44
|
+
if (args.isLoading)
|
|
45
|
+
return true;
|
|
46
|
+
if (args.locale === args.defaultLocale)
|
|
47
|
+
return true;
|
|
48
|
+
if (args.routing === "path") {
|
|
49
|
+
const stripped = stripLocalePrefix(window.location.pathname, args.allLocales);
|
|
50
|
+
if (isNonLocalizedPath(stripped, args.nonLocalizedPaths))
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}, [args.allLocales, args.defaultLocale, args.isLoading, args.locale, args.mode, args.nonLocalizedPaths, args.resolvedApiKey, args.routing]);
|
|
55
|
+
const runScan = useCallback(async () => {
|
|
56
|
+
scheduledRef.current = null;
|
|
57
|
+
if (shouldSkip())
|
|
58
|
+
return;
|
|
59
|
+
if (!document.body)
|
|
60
|
+
return;
|
|
61
|
+
if (inFlightRef.current)
|
|
62
|
+
return;
|
|
63
|
+
const ignore = new Set([...pendingRef.current, ...seenRef.current]);
|
|
64
|
+
const { misses, blurred } = scanDomForMisses({ max: MISS_MAX_PER_PAGE, ignore, blur: true });
|
|
65
|
+
if (misses.length === 0)
|
|
66
|
+
return;
|
|
67
|
+
mergeBlurred(blurred);
|
|
68
|
+
misses.forEach((miss) => pendingRef.current.add(miss.source_text));
|
|
69
|
+
inFlightRef.current = true;
|
|
70
|
+
try {
|
|
71
|
+
const response = await args.apiRef.current.reportStringMisses(args.locale, misses, {
|
|
72
|
+
sourceLocale: args.defaultLocale,
|
|
73
|
+
locales: args.allLocales,
|
|
74
|
+
});
|
|
75
|
+
if (response?.ignored) {
|
|
76
|
+
for (const miss of misses) {
|
|
77
|
+
pendingRef.current.delete(miss.source_text);
|
|
78
|
+
clearBlurForText(miss.source_text);
|
|
79
|
+
seenRef.current.add(miss.source_text);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const translations = Array.isArray(response?.translations) ? response.translations : [];
|
|
84
|
+
const pii = Array.isArray(response?.pii) ? response.pii : [];
|
|
85
|
+
const resolved = new Set();
|
|
86
|
+
pii.forEach((text) => resolved.add(text));
|
|
87
|
+
translations.forEach((row) => {
|
|
88
|
+
if (row?.source_text)
|
|
89
|
+
resolved.add(row.source_text);
|
|
90
|
+
});
|
|
91
|
+
if (translations.length > 0) {
|
|
92
|
+
const additions = translations
|
|
93
|
+
.map((row) => ({
|
|
94
|
+
source_text: row.source_text,
|
|
95
|
+
translated_text: row.translated_text,
|
|
96
|
+
source_locale: args.defaultLocale,
|
|
97
|
+
target_locale: args.locale,
|
|
98
|
+
}))
|
|
99
|
+
.filter((row) => row.source_text && row.translated_text);
|
|
100
|
+
if (additions.length > 0) {
|
|
101
|
+
addActiveTranslations(additions);
|
|
102
|
+
applyActiveTranslations(document.body);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const miss of misses) {
|
|
106
|
+
pendingRef.current.delete(miss.source_text);
|
|
107
|
+
clearBlurForText(miss.source_text);
|
|
108
|
+
if (resolved.has(miss.source_text)) {
|
|
109
|
+
seenRef.current.add(miss.source_text);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
for (const miss of misses) {
|
|
115
|
+
pendingRef.current.delete(miss.source_text);
|
|
116
|
+
clearBlurForText(miss.source_text);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
inFlightRef.current = false;
|
|
121
|
+
}
|
|
122
|
+
}, [args.apiRef, args.defaultLocale, args.locale, clearBlurForText, mergeBlurred, shouldSkip]);
|
|
123
|
+
const scheduleScan = useCallback(() => {
|
|
124
|
+
if (scheduledRef.current != null)
|
|
125
|
+
return;
|
|
126
|
+
scheduledRef.current = window.setTimeout(() => {
|
|
127
|
+
void runScan();
|
|
128
|
+
}, MISS_SCAN_THROTTLE_MS);
|
|
129
|
+
}, [runScan]);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (shouldSkip())
|
|
132
|
+
return;
|
|
133
|
+
scheduleScan();
|
|
134
|
+
if (!document.body)
|
|
135
|
+
return;
|
|
136
|
+
const observer = new MutationObserver(() => scheduleScan());
|
|
137
|
+
observer.observe(document.body, {
|
|
138
|
+
childList: true,
|
|
139
|
+
subtree: true,
|
|
140
|
+
characterData: true,
|
|
141
|
+
});
|
|
142
|
+
return () => {
|
|
143
|
+
observer.disconnect();
|
|
144
|
+
if (scheduledRef.current != null) {
|
|
145
|
+
window.clearTimeout(scheduledRef.current);
|
|
146
|
+
scheduledRef.current = null;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}, [scheduleScan, shouldSkip]);
|
|
150
|
+
}
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -59,6 +59,24 @@ export type BootstrapResponse = {
|
|
|
59
59
|
};
|
|
60
60
|
etag?: string;
|
|
61
61
|
};
|
|
62
|
+
export type MissReportItem = {
|
|
63
|
+
source_text: string;
|
|
64
|
+
semantic_context?: string | null;
|
|
65
|
+
};
|
|
66
|
+
export type MissReportResponse = {
|
|
67
|
+
translations?: Array<{
|
|
68
|
+
source_text: string;
|
|
69
|
+
translated_text: string;
|
|
70
|
+
}>;
|
|
71
|
+
pii?: string[];
|
|
72
|
+
ignored?: boolean;
|
|
73
|
+
reason?: string;
|
|
74
|
+
};
|
|
75
|
+
export type MissReportOptions = {
|
|
76
|
+
pathOrUrl?: string;
|
|
77
|
+
sourceLocale?: string;
|
|
78
|
+
locales?: string[];
|
|
79
|
+
};
|
|
62
80
|
export declare class LovalingoAPI {
|
|
63
81
|
private apiKey;
|
|
64
82
|
private apiBase;
|
|
@@ -78,6 +96,7 @@ export declare class LovalingoAPI {
|
|
|
78
96
|
critical_count?: number;
|
|
79
97
|
critical_hash?: string;
|
|
80
98
|
}): Promise<void>;
|
|
99
|
+
reportStringMisses(targetLocale: string, misses: MissReportItem[], opts?: MissReportOptions): Promise<MissReportResponse | null>;
|
|
81
100
|
fetchTranslations(sourceLocale: string, targetLocale: string): Promise<Translation[]>;
|
|
82
101
|
fetchBundle(localeHint: string, pathOrUrl?: string): Promise<{
|
|
83
102
|
map: Record<string, string>;
|
package/dist/utils/api.js
CHANGED
|
@@ -143,6 +143,42 @@ export class LovalingoAPI {
|
|
|
143
143
|
// Ignore tracking errors.
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
async reportStringMisses(targetLocale, misses, opts) {
|
|
147
|
+
try {
|
|
148
|
+
if (!this.hasApiKey())
|
|
149
|
+
return null;
|
|
150
|
+
if (!Array.isArray(misses) || misses.length === 0)
|
|
151
|
+
return null;
|
|
152
|
+
const pathParam = this.buildPathParam(opts?.pathOrUrl);
|
|
153
|
+
const response = await fetch(`${this.apiBase}/functions/v1/misses`, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: { "Content-Type": "application/json" },
|
|
156
|
+
body: JSON.stringify({
|
|
157
|
+
key: this.apiKey,
|
|
158
|
+
locale: targetLocale,
|
|
159
|
+
path: pathParam,
|
|
160
|
+
source_locale: opts?.sourceLocale,
|
|
161
|
+
locales: Array.isArray(opts?.locales) ? opts?.locales : undefined,
|
|
162
|
+
misses,
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
if (this.isActivationRequiredResponse(response)) {
|
|
166
|
+
this.logActivationRequired("reportStringMisses", response);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
if (!response.ok)
|
|
170
|
+
return null;
|
|
171
|
+
const data = await response.json();
|
|
172
|
+
if (this.isActivationRequiredResponse(response, data)) {
|
|
173
|
+
this.logActivationRequired("reportStringMisses", response);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return data;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
146
182
|
async fetchTranslations(sourceLocale, targetLocale) {
|
|
147
183
|
try {
|
|
148
184
|
if (!this.hasApiKey()) {
|
|
@@ -39,6 +39,14 @@ export type DomScanResult = {
|
|
|
39
39
|
};
|
|
40
40
|
truncated: boolean;
|
|
41
41
|
};
|
|
42
|
+
export type DomMiss = {
|
|
43
|
+
source_text: string;
|
|
44
|
+
semantic_context: string;
|
|
45
|
+
};
|
|
46
|
+
export type DomMissScanResult = {
|
|
47
|
+
misses: DomMiss[];
|
|
48
|
+
blurred: Map<string, Set<HTMLElement>>;
|
|
49
|
+
};
|
|
42
50
|
export type CriticalFingerprint = {
|
|
43
51
|
critical_count: number;
|
|
44
52
|
critical_hash: string;
|
|
@@ -53,6 +61,13 @@ export declare function stopMarkerEngine(): void;
|
|
|
53
61
|
export declare function getMarkerStats(): MarkerStats;
|
|
54
62
|
export declare function setMarkerEngineExclusions(exclusions: Exclusion[] | null): void;
|
|
55
63
|
export declare function setActiveTranslations(translations: Translation[] | null): void;
|
|
64
|
+
export declare function addActiveTranslations(translations: Translation[] | Record<string, string> | null): number;
|
|
56
65
|
export declare function applyActiveTranslations(root?: ParentNode | null): number;
|
|
66
|
+
export declare function clearMissBlur(elements: Iterable<HTMLElement>): void;
|
|
67
|
+
export declare function scanDomForMisses(opts: {
|
|
68
|
+
max: number;
|
|
69
|
+
ignore?: Set<string>;
|
|
70
|
+
blur?: boolean;
|
|
71
|
+
}): DomMissScanResult;
|
|
57
72
|
export declare function restoreDom(root?: ParentNode | null): void;
|
|
58
73
|
export {};
|
|
@@ -10,6 +10,8 @@ const ATTRIBUTE_MARKS = [
|
|
|
10
10
|
{ attr: "aria-label", marker: "data-lovalingo-aria-label-original" },
|
|
11
11
|
{ attr: "placeholder", marker: "data-lovalingo-placeholder-original" },
|
|
12
12
|
];
|
|
13
|
+
const MISS_BLUR_ATTR = "data-lovalingo-miss-blur";
|
|
14
|
+
const MISS_BLUR_STYLE_ID = "lovalingo-miss-blur-style";
|
|
13
15
|
const unsafeSelector = Array.from(UNSAFE_CONTAINER_TAGS).join(",");
|
|
14
16
|
let observer = null;
|
|
15
17
|
let scheduled = null;
|
|
@@ -176,6 +178,31 @@ function getOrInitAttrOriginal(el, attr) {
|
|
|
176
178
|
map.set(attr, value);
|
|
177
179
|
return value;
|
|
178
180
|
}
|
|
181
|
+
// Why: avoid blurring large containers; keep the effect scoped to leaf text nodes.
|
|
182
|
+
function shouldBlurElement(el) {
|
|
183
|
+
if (!el)
|
|
184
|
+
return false;
|
|
185
|
+
if (el.tagName === "HTML" || el.tagName === "BODY")
|
|
186
|
+
return false;
|
|
187
|
+
if (el.childElementCount > 0)
|
|
188
|
+
return false;
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
function ensureMissBlurStyles() {
|
|
192
|
+
if (typeof document === "undefined")
|
|
193
|
+
return;
|
|
194
|
+
if (document.getElementById(MISS_BLUR_STYLE_ID))
|
|
195
|
+
return;
|
|
196
|
+
const style = document.createElement("style");
|
|
197
|
+
style.id = MISS_BLUR_STYLE_ID;
|
|
198
|
+
style.textContent =
|
|
199
|
+
`[${MISS_BLUR_ATTR}="1"] {` +
|
|
200
|
+
"filter: blur(2px);" +
|
|
201
|
+
"transition: filter 0.18s ease-out;" +
|
|
202
|
+
"will-change: filter;" +
|
|
203
|
+
"}";
|
|
204
|
+
document.head.appendChild(style);
|
|
205
|
+
}
|
|
179
206
|
function isInViewport(rect, viewportHeight, bufferPx) {
|
|
180
207
|
if (!rect)
|
|
181
208
|
return false;
|
|
@@ -552,6 +579,38 @@ export function setActiveTranslations(translations) {
|
|
|
552
579
|
}
|
|
553
580
|
activeTranslationMap = map;
|
|
554
581
|
}
|
|
582
|
+
export function addActiveTranslations(translations) {
|
|
583
|
+
if (!translations)
|
|
584
|
+
return 0;
|
|
585
|
+
const map = activeTranslationMap ?? new Map();
|
|
586
|
+
let added = 0;
|
|
587
|
+
if (Array.isArray(translations)) {
|
|
588
|
+
for (const t of translations) {
|
|
589
|
+
const source = normalizeWhitespace((t?.source_text || "").toString());
|
|
590
|
+
const translated = (t?.translated_text ?? "").toString();
|
|
591
|
+
if (!source || !translated)
|
|
592
|
+
continue;
|
|
593
|
+
if (map.get(source) === translated)
|
|
594
|
+
continue;
|
|
595
|
+
map.set(source, translated);
|
|
596
|
+
added += 1;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
for (const [keyRaw, valueRaw] of Object.entries(translations || {})) {
|
|
601
|
+
const source = normalizeWhitespace((keyRaw || "").toString());
|
|
602
|
+
const translated = (valueRaw ?? "").toString();
|
|
603
|
+
if (!source || !translated)
|
|
604
|
+
continue;
|
|
605
|
+
if (map.get(source) === translated)
|
|
606
|
+
continue;
|
|
607
|
+
map.set(source, translated);
|
|
608
|
+
added += 1;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
activeTranslationMap = map;
|
|
612
|
+
return added;
|
|
613
|
+
}
|
|
555
614
|
function applyTranslationMap(bundle, root) {
|
|
556
615
|
if (!root)
|
|
557
616
|
return 0;
|
|
@@ -641,6 +700,100 @@ export function applyActiveTranslations(root = document.body) {
|
|
|
641
700
|
}
|
|
642
701
|
return applied;
|
|
643
702
|
}
|
|
703
|
+
export function clearMissBlur(elements) {
|
|
704
|
+
for (const el of elements) {
|
|
705
|
+
if (!el)
|
|
706
|
+
continue;
|
|
707
|
+
if (el.getAttribute(MISS_BLUR_ATTR) !== "1")
|
|
708
|
+
continue;
|
|
709
|
+
el.removeAttribute(MISS_BLUR_ATTR);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
export function scanDomForMisses(opts) {
|
|
713
|
+
const root = document.body;
|
|
714
|
+
const misses = [];
|
|
715
|
+
const blurred = new Map();
|
|
716
|
+
if (!root || !activeTranslationMap || activeTranslationMap.size === 0) {
|
|
717
|
+
return { misses, blurred };
|
|
718
|
+
}
|
|
719
|
+
const max = Math.max(0, Math.floor(opts.max || 0));
|
|
720
|
+
if (max <= 0)
|
|
721
|
+
return { misses, blurred };
|
|
722
|
+
if (opts.blur)
|
|
723
|
+
ensureMissBlurStyles();
|
|
724
|
+
const ignore = opts.ignore || new Set();
|
|
725
|
+
const seen = new Set();
|
|
726
|
+
const recordMiss = (text, context, el) => {
|
|
727
|
+
if (!text || seen.has(text) || ignore.has(text))
|
|
728
|
+
return;
|
|
729
|
+
if (activeTranslationMap?.has(text))
|
|
730
|
+
return;
|
|
731
|
+
if (misses.length >= max)
|
|
732
|
+
return;
|
|
733
|
+
seen.add(text);
|
|
734
|
+
misses.push({ source_text: text, semantic_context: context });
|
|
735
|
+
if (opts.blur && el instanceof HTMLElement && shouldBlurElement(el)) {
|
|
736
|
+
el.setAttribute(MISS_BLUR_ATTR, "1");
|
|
737
|
+
let set = blurred.get(text);
|
|
738
|
+
if (!set) {
|
|
739
|
+
set = new Set();
|
|
740
|
+
blurred.set(text, set);
|
|
741
|
+
}
|
|
742
|
+
set.add(el);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
746
|
+
let node = walker.nextNode();
|
|
747
|
+
while (node && misses.length < max) {
|
|
748
|
+
if (node.nodeType !== Node.TEXT_NODE) {
|
|
749
|
+
node = walker.nextNode();
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
const textNode = node;
|
|
753
|
+
const parent = textNode.parentElement;
|
|
754
|
+
if (!parent || isExcludedElement(parent) || findUnsafeContainer(parent)) {
|
|
755
|
+
node = walker.nextNode();
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
const raw = textNode.nodeValue || "";
|
|
759
|
+
const trimmed = raw.trim();
|
|
760
|
+
if (!trimmed || !isTranslatableText(trimmed)) {
|
|
761
|
+
node = walker.nextNode();
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const original = getOrInitTextOriginal(textNode, parent);
|
|
765
|
+
const key = normalizeWhitespace(original.trimmed);
|
|
766
|
+
if (key) {
|
|
767
|
+
recordMiss(key, "text", parent);
|
|
768
|
+
}
|
|
769
|
+
node = walker.nextNode();
|
|
770
|
+
}
|
|
771
|
+
if (misses.length < max) {
|
|
772
|
+
const nodes = root.querySelectorAll("[title],[aria-label],[placeholder]");
|
|
773
|
+
nodes.forEach((el) => {
|
|
774
|
+
if (misses.length >= max)
|
|
775
|
+
return;
|
|
776
|
+
if (isExcludedElement(el) || findUnsafeContainer(el))
|
|
777
|
+
return;
|
|
778
|
+
for (const { attr } of ATTRIBUTE_MARKS) {
|
|
779
|
+
if (misses.length >= max)
|
|
780
|
+
break;
|
|
781
|
+
const value = el.getAttribute(attr);
|
|
782
|
+
if (!value)
|
|
783
|
+
continue;
|
|
784
|
+
const trimmed = value.trim();
|
|
785
|
+
if (!trimmed || !isTranslatableText(trimmed))
|
|
786
|
+
continue;
|
|
787
|
+
const original = normalizeWhitespace(getOrInitAttrOriginal(el, attr));
|
|
788
|
+
if (!original)
|
|
789
|
+
continue;
|
|
790
|
+
const context = attr === "title" ? "attr:title" : attr === "aria-label" ? "attr:aria-label" : "attr:placeholder";
|
|
791
|
+
recordMiss(original, context);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
return { misses, blurred };
|
|
796
|
+
}
|
|
644
797
|
export function restoreDom(root = document.body) {
|
|
645
798
|
if (!root)
|
|
646
799
|
return;
|
package/package.json
CHANGED