@lovalingo/lovalingo 0.5.23 → 0.5.25
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.
|
@@ -556,7 +556,6 @@ navigateRef, // For path mode routing
|
|
|
556
556
|
void (async () => {
|
|
557
557
|
if (!allLocales.includes(newLocale))
|
|
558
558
|
return;
|
|
559
|
-
const previousLocale = locale; // Capture current locale before switching
|
|
560
559
|
// Save to localStorage
|
|
561
560
|
try {
|
|
562
561
|
localStorage.setItem(LOCALE_STORAGE_KEY, newLocale);
|
|
@@ -567,48 +566,39 @@ navigateRef, // For path mode routing
|
|
|
567
566
|
isInternalNavigationRef.current = true;
|
|
568
567
|
// Prevent MutationObserver work during the switch to avoid React conflicts
|
|
569
568
|
isNavigatingRef.current = true;
|
|
569
|
+
// Why: force a full reload on locale switches to avoid mixed-language DOM residues.
|
|
570
|
+
let nextUrl = "";
|
|
570
571
|
// Update URL based on routing strategy
|
|
571
572
|
if (routing === 'path') {
|
|
572
573
|
const stripped = stripLocalePrefix(window.location.pathname, allLocales);
|
|
573
574
|
if (isNonLocalizedPath(stripped, routingConfig.nonLocalizedPaths)) {
|
|
574
575
|
// Why: switching languages must not rewrite non-localized routes like "/auth" to "/de/auth".
|
|
575
|
-
|
|
576
|
-
isNavigatingRef.current = false;
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
const pathParts = window.location.pathname.split('/').filter(Boolean);
|
|
580
|
-
// Strip existing locale
|
|
581
|
-
if (allLocales.includes(pathParts[0])) {
|
|
582
|
-
pathParts.shift();
|
|
583
|
-
}
|
|
584
|
-
// Build new path with new locale
|
|
585
|
-
const basePath = pathParts.join('/');
|
|
586
|
-
const newPath = `/${newLocale}${basePath ? '/' + basePath : ''}${window.location.search}${window.location.hash}`;
|
|
587
|
-
// Prefer React Router navigation when available, but gracefully fallback for non-React-Router apps
|
|
588
|
-
const navigate = navigateRef?.current;
|
|
589
|
-
if (navigate) {
|
|
590
|
-
navigate(newPath);
|
|
576
|
+
nextUrl = window.location.href;
|
|
591
577
|
}
|
|
592
578
|
else {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
window.location.assign(newPath);
|
|
579
|
+
const pathParts = window.location.pathname.split('/').filter(Boolean);
|
|
580
|
+
// Strip existing locale
|
|
581
|
+
if (allLocales.includes(pathParts[0])) {
|
|
582
|
+
pathParts.shift();
|
|
598
583
|
}
|
|
584
|
+
// Build new path with new locale
|
|
585
|
+
const basePath = pathParts.join('/');
|
|
586
|
+
nextUrl = `/${newLocale}${basePath ? '/' + basePath : ''}${window.location.search}${window.location.hash}`;
|
|
599
587
|
}
|
|
600
588
|
}
|
|
601
589
|
else if (routing === 'query') {
|
|
602
590
|
const url = new URL(window.location.href);
|
|
603
591
|
url.searchParams.set('t', newLocale);
|
|
604
|
-
|
|
592
|
+
nextUrl = url.toString();
|
|
605
593
|
}
|
|
606
|
-
|
|
607
|
-
|
|
594
|
+
if (!nextUrl)
|
|
595
|
+
nextUrl = window.location.href;
|
|
596
|
+
window.location.assign(nextUrl);
|
|
597
|
+
return;
|
|
608
598
|
})().finally(() => {
|
|
609
599
|
isInternalNavigationRef.current = false;
|
|
610
600
|
});
|
|
611
|
-
}, [allLocales,
|
|
601
|
+
}, [allLocales, locale, routing, routingConfig.nonLocalizedPaths]);
|
|
612
602
|
// No string-level "misses" from the client: the pipeline (Browser Rendering) is source-of-truth for string discovery.
|
|
613
603
|
// Why: prevent init/load effects from re-running (and calling bootstrap/bundle again) when loadData changes due to state updates.
|
|
614
604
|
const loadDataRef = useRef(loadData);
|
|
@@ -6,6 +6,29 @@ import { isNonLocalizedPath, stripLocalePrefix } from "../../utils/nonLocalizedP
|
|
|
6
6
|
const MISS_SCAN_THROTTLE_MS = 600;
|
|
7
7
|
// Why: allow large pages to report plenty of misses while keeping payloads bounded.
|
|
8
8
|
const MISS_MAX_PER_PAGE = 500;
|
|
9
|
+
function looksLike404Page() {
|
|
10
|
+
if (typeof document === "undefined")
|
|
11
|
+
return false;
|
|
12
|
+
// 1. Meta tag (Explicit opt-in for SPAs)
|
|
13
|
+
const meta = document.querySelector('meta[name="lovalingo:status"]');
|
|
14
|
+
if (meta && meta.getAttribute("content") === "404")
|
|
15
|
+
return true;
|
|
16
|
+
// 2. Title Heuristics
|
|
17
|
+
const title = (document.title || "").toLowerCase();
|
|
18
|
+
// Why: detect common 404 title patterns to avoid reporting misses on ghost pages.
|
|
19
|
+
if (/page not found|seite nicht gefunden|article not found|error 404|page missing|does not exist/.test(title))
|
|
20
|
+
return true;
|
|
21
|
+
// 3. H1 Heuristics
|
|
22
|
+
const h1 = document.querySelector("h1");
|
|
23
|
+
if (h1) {
|
|
24
|
+
const txt = (h1.innerText || "").toLowerCase();
|
|
25
|
+
if (/page not found|seite nicht gefunden|article not found|error 404/.test(txt))
|
|
26
|
+
return true;
|
|
27
|
+
if (txt.includes("404") && txt.length < 50)
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
9
32
|
export function useStringMissReporting(args) {
|
|
10
33
|
const pendingRef = useRef(new Set());
|
|
11
34
|
const seenRef = useRef(new Set());
|
|
@@ -18,6 +41,8 @@ export function useStringMissReporting(args) {
|
|
|
18
41
|
const shouldSkip = useCallback(() => {
|
|
19
42
|
if (typeof window === "undefined" || typeof document === "undefined")
|
|
20
43
|
return true;
|
|
44
|
+
if (looksLike404Page())
|
|
45
|
+
return true;
|
|
21
46
|
const disableLiveMisses = Boolean(window.__lovalingoDisableMisses);
|
|
22
47
|
if (disableLiveMisses)
|
|
23
48
|
return true;
|
package/dist/utils/api.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { warnDebug, errorDebug } from './logger';
|
|
2
2
|
const OK_HTTP_STATUSES = new Set([200, 201]);
|
|
3
|
+
const NOT_FOUND_TITLE_HINTS = /404|not found|page not found|page missing|does not exist|error 404/i;
|
|
3
4
|
function getNavigationResponseStatus() {
|
|
4
5
|
if (typeof performance === "undefined" || typeof performance.getEntriesByType !== "function")
|
|
5
6
|
return null;
|
|
@@ -19,6 +20,19 @@ function isOkHttpStatus(status) {
|
|
|
19
20
|
return true;
|
|
20
21
|
return OK_HTTP_STATUSES.has(status);
|
|
21
22
|
}
|
|
23
|
+
function looksLikeNotFoundDocument() {
|
|
24
|
+
if (typeof document === "undefined")
|
|
25
|
+
return false;
|
|
26
|
+
const title = (document.title || "").toString().trim().toLowerCase();
|
|
27
|
+
if (title && NOT_FOUND_TITLE_HINTS.test(title))
|
|
28
|
+
return true;
|
|
29
|
+
const bodyText = (document.body?.textContent || "").toString().slice(0, 2000).toLowerCase();
|
|
30
|
+
if (!bodyText)
|
|
31
|
+
return false;
|
|
32
|
+
const has404 = /\b404\b/.test(bodyText);
|
|
33
|
+
const hasNotFound = /not found|page not found|page missing|does not exist|error 404/.test(bodyText);
|
|
34
|
+
return has404 && hasNotFound;
|
|
35
|
+
}
|
|
22
36
|
function normalizeApiBase(raw) {
|
|
23
37
|
const input = (raw || "").toString().trim();
|
|
24
38
|
if (!input)
|
|
@@ -168,6 +182,9 @@ export class LovalingoAPI {
|
|
|
168
182
|
const status = getNavigationResponseStatus();
|
|
169
183
|
if (!isOkHttpStatus(status))
|
|
170
184
|
return;
|
|
185
|
+
// Why: avoid tracking SPA soft-404s (HTTP 200 + "not found" content) so ghost pages don't reappear.
|
|
186
|
+
if (looksLikeNotFoundDocument())
|
|
187
|
+
return;
|
|
171
188
|
const params = new URLSearchParams();
|
|
172
189
|
params.set("key", this.apiKey);
|
|
173
190
|
params.set("path", pathOrUrl);
|
|
@@ -200,6 +217,9 @@ export class LovalingoAPI {
|
|
|
200
217
|
if (!isOkHttpStatus(status)) {
|
|
201
218
|
return { ignored: true, reason: "http_status" };
|
|
202
219
|
}
|
|
220
|
+
if (looksLikeNotFoundDocument()) {
|
|
221
|
+
return { ignored: true, reason: "soft_404" };
|
|
222
|
+
}
|
|
203
223
|
const pathParam = this.buildPathParam(opts?.pathOrUrl);
|
|
204
224
|
const response = await fetch(`${this.apiBase}/functions/v1/misses`, {
|
|
205
225
|
method: "POST",
|
package/package.json
CHANGED