@lovalingo/lovalingo 0.5.5 → 0.5.7

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.
@@ -0,0 +1,81 @@
1
+ import { useEffect } from "react";
2
+ import { processPath } from "../../utils/pathNormalizer";
3
+ export function useNavigationPrefetch({ resolvedApiKey, apiBase, defaultLocale, locale, routing, allLocales, enhancedPathConfig, }) {
4
+ // Navigation prefetch: warm the HTTP cache for bootstrap + page bundle before the user clicks (reduces EN→FR flash on SPA route changes).
5
+ useEffect(() => {
6
+ if (!resolvedApiKey)
7
+ return;
8
+ if (typeof window === "undefined" || typeof document === "undefined")
9
+ return;
10
+ const connection = navigator?.connection;
11
+ if (connection?.saveData)
12
+ return;
13
+ if (typeof connection?.effectiveType === "string" && /(^|-)2g$/.test(connection.effectiveType))
14
+ return;
15
+ const prefetched = new Set();
16
+ // Why: cap speculative requests to avoid flooding the network on pages with many links.
17
+ const maxPrefetch = 40;
18
+ const isAssetPath = (pathname) => {
19
+ if (pathname === "/robots.txt" || pathname === "/sitemap.xml")
20
+ return true;
21
+ if (pathname.startsWith("/.well-known/"))
22
+ return true;
23
+ return /\.(?:png|jpg|jpeg|gif|svg|webp|avif|ico|css|js|map|json|xml|txt|pdf|zip|gz|br|woff2?|ttf|eot)$/i.test(pathname);
24
+ };
25
+ const pickLocaleForUrl = (url) => {
26
+ if (routing === "path") {
27
+ const segment = url.pathname.split("/")[1] || "";
28
+ if (segment && allLocales.includes(segment))
29
+ return segment;
30
+ return locale;
31
+ }
32
+ const q = url.searchParams.get("t") || url.searchParams.get("locale");
33
+ if (q && allLocales.includes(q))
34
+ return q;
35
+ return locale;
36
+ };
37
+ const onIntent = (event) => {
38
+ if (prefetched.size >= maxPrefetch)
39
+ return;
40
+ const target = event.target;
41
+ const anchor = target?.closest?.("a[href]");
42
+ if (!anchor)
43
+ return;
44
+ const href = anchor.getAttribute("href") || "";
45
+ if (!href || /^(?:#|mailto:|tel:|sms:|javascript:)/i.test(href))
46
+ return;
47
+ let url;
48
+ try {
49
+ url = new URL(href, window.location.origin);
50
+ }
51
+ catch {
52
+ return;
53
+ }
54
+ if (url.origin !== window.location.origin)
55
+ return;
56
+ if (isAssetPath(url.pathname))
57
+ return;
58
+ const targetLocale = pickLocaleForUrl(url);
59
+ if (!targetLocale || targetLocale === defaultLocale)
60
+ return;
61
+ const normalizedPath = processPath(url.pathname, enhancedPathConfig);
62
+ const key = `${targetLocale}:${normalizedPath}`;
63
+ if (prefetched.has(key))
64
+ return;
65
+ prefetched.add(key);
66
+ const pathParam = `${url.pathname}${url.search}`;
67
+ const bootstrapUrl = `${apiBase}/functions/v1/bootstrap?key=${encodeURIComponent(resolvedApiKey)}&locale=${encodeURIComponent(targetLocale)}&path=${encodeURIComponent(pathParam)}`;
68
+ const bundleUrl = `${apiBase}/functions/v1/bundle?key=${encodeURIComponent(resolvedApiKey)}&locale=${encodeURIComponent(targetLocale)}&path=${encodeURIComponent(pathParam)}`;
69
+ void fetch(bootstrapUrl, { cache: "force-cache" }).catch(() => undefined);
70
+ void fetch(bundleUrl, { cache: "force-cache" }).catch(() => undefined);
71
+ };
72
+ document.addEventListener("pointerover", onIntent, { passive: true });
73
+ document.addEventListener("touchstart", onIntent, { passive: true });
74
+ document.addEventListener("focusin", onIntent);
75
+ return () => {
76
+ document.removeEventListener("pointerover", onIntent);
77
+ document.removeEventListener("touchstart", onIntent);
78
+ document.removeEventListener("focusin", onIntent);
79
+ };
80
+ }, [allLocales, apiBase, defaultLocale, enhancedPathConfig, locale, resolvedApiKey, routing]);
81
+ }
@@ -0,0 +1,10 @@
1
+ import type React from "react";
2
+ import type { LovalingoAPI } from "../../utils/api";
3
+ type UsePageviewTrackingOptions = {
4
+ apiRef: React.MutableRefObject<LovalingoAPI>;
5
+ resolvedApiKey: string;
6
+ };
7
+ export declare function usePageviewTracking({ apiRef, resolvedApiKey }: UsePageviewTrackingOptions): {
8
+ trackPageviewOnce: (path: string) => void;
9
+ };
10
+ export {};
@@ -0,0 +1,44 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import { getCriticalFingerprint } from "../../utils/markerEngine";
3
+ export function usePageviewTracking({ apiRef, resolvedApiKey }) {
4
+ const lastPageviewRef = useRef("");
5
+ const lastPageviewFingerprintRef = useRef("");
6
+ const pageviewFingerprintTimeoutRef = useRef(null);
7
+ const pageviewFingerprintRetryTimeoutRef = useRef(null);
8
+ useEffect(() => {
9
+ lastPageviewRef.current = "";
10
+ lastPageviewFingerprintRef.current = "";
11
+ }, [resolvedApiKey]);
12
+ const trackPageviewOnce = useCallback((path) => {
13
+ const next = (path || "").toString();
14
+ if (!next)
15
+ return;
16
+ if (lastPageviewRef.current === next)
17
+ return;
18
+ lastPageviewRef.current = next;
19
+ apiRef.current.trackPageview(next);
20
+ const trySendFingerprint = () => {
21
+ if (typeof window === "undefined")
22
+ return;
23
+ const markersReady = window.__lovalingoMarkersReady === true;
24
+ if (!markersReady)
25
+ return;
26
+ const fp = getCriticalFingerprint();
27
+ if (!fp || fp.critical_count <= 0)
28
+ return;
29
+ const signature = `${next}|${fp.critical_hash}|${fp.critical_count}`;
30
+ if (lastPageviewFingerprintRef.current === signature)
31
+ return;
32
+ lastPageviewFingerprintRef.current = signature;
33
+ apiRef.current.trackPageview(next, fp);
34
+ };
35
+ if (pageviewFingerprintTimeoutRef.current != null)
36
+ window.clearTimeout(pageviewFingerprintTimeoutRef.current);
37
+ if (pageviewFingerprintRetryTimeoutRef.current != null)
38
+ window.clearTimeout(pageviewFingerprintRetryTimeoutRef.current);
39
+ // Why: wait briefly for markers/content to settle before computing a critical fingerprint for change detection.
40
+ pageviewFingerprintTimeoutRef.current = window.setTimeout(trySendFingerprint, 800);
41
+ pageviewFingerprintRetryTimeoutRef.current = window.setTimeout(trySendFingerprint, 2000);
42
+ }, [apiRef]);
43
+ return { trackPageviewOnce };
44
+ }
@@ -0,0 +1,5 @@
1
+ export declare const PREHIDE_FAILSAFE_MS = 1700;
2
+ export declare function usePrehide(): {
3
+ enablePrehide: (bgColor: string) => void;
4
+ disablePrehide: () => void;
5
+ };
@@ -0,0 +1,72 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ // Why: avoid long blank screens on blocked/untranslated routes by always revealing the original page quickly.
3
+ export const PREHIDE_FAILSAFE_MS = 1700;
4
+ export function usePrehide() {
5
+ const prehideStateRef = useRef({
6
+ active: false,
7
+ timeoutId: null,
8
+ startedAtMs: null,
9
+ prevHtmlVisibility: "",
10
+ prevBodyVisibility: "",
11
+ prevHtmlBg: "",
12
+ prevBodyBg: "",
13
+ });
14
+ const forceDisablePrehide = useCallback(() => {
15
+ if (typeof document === "undefined")
16
+ return;
17
+ const html = document.documentElement;
18
+ const body = document.body;
19
+ if (!html || !body)
20
+ return;
21
+ const state = prehideStateRef.current;
22
+ if (state.timeoutId != null) {
23
+ window.clearTimeout(state.timeoutId);
24
+ state.timeoutId = null;
25
+ }
26
+ if (!state.active)
27
+ return;
28
+ state.active = false;
29
+ state.startedAtMs = null;
30
+ html.style.visibility = state.prevHtmlVisibility;
31
+ body.style.visibility = state.prevBodyVisibility;
32
+ html.style.backgroundColor = state.prevHtmlBg;
33
+ body.style.backgroundColor = state.prevBodyBg;
34
+ }, []);
35
+ const enablePrehide = useCallback((bgColor) => {
36
+ if (typeof document === "undefined")
37
+ return;
38
+ const html = document.documentElement;
39
+ const body = document.body;
40
+ if (!html || !body)
41
+ return;
42
+ const state = prehideStateRef.current;
43
+ // Why: avoid "perma-hidden" pages when repeated navigation/errors keep prehide active; always hard-stop after a few seconds.
44
+ if (state.active && state.startedAtMs != null && Date.now() - state.startedAtMs > PREHIDE_FAILSAFE_MS * 3) {
45
+ forceDisablePrehide();
46
+ }
47
+ if (!state.active) {
48
+ state.active = true;
49
+ state.startedAtMs = Date.now();
50
+ state.prevHtmlVisibility = html.style.visibility || "";
51
+ state.prevBodyVisibility = body.style.visibility || "";
52
+ state.prevHtmlBg = html.style.backgroundColor || "";
53
+ state.prevBodyBg = body.style.backgroundColor || "";
54
+ }
55
+ html.style.visibility = "hidden";
56
+ body.style.visibility = "hidden";
57
+ if (bgColor) {
58
+ html.style.backgroundColor = bgColor;
59
+ body.style.backgroundColor = bgColor;
60
+ }
61
+ if (state.timeoutId != null) {
62
+ return;
63
+ }
64
+ // Why: avoid a "perma-hide" when navigation events repeatedly re-trigger prehide and keep extending the timeout.
65
+ state.timeoutId = window.setTimeout(() => forceDisablePrehide(), PREHIDE_FAILSAFE_MS);
66
+ }, [forceDisablePrehide]);
67
+ const disablePrehide = forceDisablePrehide;
68
+ useEffect(() => {
69
+ return () => disablePrehide();
70
+ }, [disablePrehide]);
71
+ return { enablePrehide, disablePrehide };
72
+ }
@@ -0,0 +1,7 @@
1
+ type UseSitemapLinkTagOptions = {
2
+ enabled: boolean;
3
+ resolvedApiKey: string;
4
+ isSeoActive: () => boolean;
5
+ };
6
+ export declare function useSitemapLinkTag({ enabled, resolvedApiKey, isSeoActive }: UseSitemapLinkTagOptions): void;
7
+ export {};
@@ -0,0 +1,28 @@
1
+ import { useEffect } from "react";
2
+ export function useSitemapLinkTag({ enabled, resolvedApiKey, isSeoActive }) {
3
+ // Auto-inject sitemap link tag
4
+ useEffect(() => {
5
+ if (enabled && resolvedApiKey && isSeoActive()) {
6
+ // Prefer same-origin /sitemap.xml so crawlers discover the canonical sitemap URL.
7
+ // Reminder: /sitemap.xml should be published by the host app (recommended: build-time copy from Lovalingo CDN).
8
+ const sitemapUrl = `${window.location.origin}/sitemap.xml`;
9
+ // Check if link already exists to avoid duplicates
10
+ const existingLink = document.querySelector(`link[rel="sitemap"][href="${sitemapUrl}"]`);
11
+ if (existingLink)
12
+ return;
13
+ // Create and inject link tag
14
+ const link = document.createElement("link");
15
+ link.rel = "sitemap";
16
+ link.type = "application/xml";
17
+ link.href = sitemapUrl;
18
+ document.head.appendChild(link);
19
+ // Cleanup on unmount
20
+ return () => {
21
+ const linkToRemove = document.querySelector(`link[rel="sitemap"][href="${sitemapUrl}"]`);
22
+ if (linkToRemove) {
23
+ document.head.removeChild(linkToRemove);
24
+ }
25
+ };
26
+ }
27
+ }, [enabled, resolvedApiKey, isSeoActive]);
28
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.5.5";
1
+ export declare const VERSION = "0.5.6";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.5.5";
1
+ export const VERSION = "0.5.6";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovalingo/lovalingo",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
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",