@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
- setLocaleState(newLocale);
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
- try {
594
- window.history.pushState({}, '', newPath);
595
- }
596
- catch {
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
- window.history.pushState({}, '', url.toString());
592
+ nextUrl = url.toString();
605
593
  }
606
- setLocaleState(newLocale);
607
- await loadData(newLocale, previousLocale);
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, defaultLocale, locale, routing, loadData, navigateRef, routingConfig.nonLocalizedPaths]);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovalingo/lovalingo",
3
- "version": "0.5.23",
3
+ "version": "0.5.25",
4
4
  "description": "React translation runtime with i18n routing, deterministic bundles + DOM rules, and zero-flash rendering.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",