@lovalingo/lovalingo 0.3.2 → 0.3.4
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.
|
@@ -9,6 +9,7 @@ import { processPath } from '../utils/pathNormalizer';
|
|
|
9
9
|
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
10
10
|
const LOCALE_STORAGE_KEY = 'Lovalingo_locale';
|
|
11
11
|
const LOADING_BG_STORAGE_PREFIX = "Lovalingo_loading_bg_color";
|
|
12
|
+
const BRANDING_STORAGE_PREFIX = "Lovalingo_branding_enabled";
|
|
12
13
|
const CRITICAL_CACHE_PREFIX = "Lovalingo_critical_v0_3";
|
|
13
14
|
export const LovalingoProvider = ({ children, apiKey: apiKeyProp, publicAnonKey, defaultLocale, locales, apiBase = 'https://leuskvkajliuzalrlwhw.supabase.co', routing = 'query', // Default to query mode (backward compatible)
|
|
14
15
|
autoPrefixLinks = true, autoApplyRules = true, switcherPosition = 'bottom-right', switcherOffsetY = 20, switcherTheme = 'dark', editMode: initialEditMode = false, editKey = 'KeyE', pathNormalization = { enabled: true }, // Enable by default
|
|
@@ -73,6 +74,10 @@ navigateRef, // For path mode routing
|
|
|
73
74
|
apiRef.current = new LovalingoAPI(resolvedApiKey, apiBase, enhancedPathConfig);
|
|
74
75
|
}, [apiBase, enhancedPathConfig, resolvedApiKey]);
|
|
75
76
|
const [entitlements, setEntitlements] = useState(() => apiRef.current.getEntitlements());
|
|
77
|
+
const lastPageviewRef = useRef("");
|
|
78
|
+
const historyPatchedRef = useRef(false);
|
|
79
|
+
const originalHistoryRef = useRef(null);
|
|
80
|
+
const onNavigateRef = useRef(() => undefined);
|
|
76
81
|
const retryTimeoutRef = useRef(null);
|
|
77
82
|
const isNavigatingRef = useRef(false);
|
|
78
83
|
const isInternalNavigationRef = useRef(false);
|
|
@@ -80,6 +85,21 @@ navigateRef, // For path mode routing
|
|
|
80
85
|
const exclusionsCacheRef = useRef(null);
|
|
81
86
|
const domRulesCacheRef = useRef(new Map());
|
|
82
87
|
const loadingBgStorageKey = `${LOADING_BG_STORAGE_PREFIX}:${resolvedApiKey || "anonymous"}`;
|
|
88
|
+
const brandingStorageKey = `${BRANDING_STORAGE_PREFIX}:${resolvedApiKey || "anonymous"}`;
|
|
89
|
+
const readBrandingCache = () => {
|
|
90
|
+
try {
|
|
91
|
+
const cached = (localStorage.getItem(brandingStorageKey) || "").trim();
|
|
92
|
+
if (cached === "0")
|
|
93
|
+
return false;
|
|
94
|
+
if (cached === "1")
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// ignore
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
};
|
|
102
|
+
const [brandingEnabled, setBrandingEnabled] = useState(readBrandingCache);
|
|
83
103
|
const prehideStateRef = useRef({
|
|
84
104
|
active: false,
|
|
85
105
|
timeoutId: null,
|
|
@@ -110,6 +130,30 @@ navigateRef, // For path mode routing
|
|
|
110
130
|
// ignore
|
|
111
131
|
}
|
|
112
132
|
}, [loadingBgStorageKey]);
|
|
133
|
+
const setCachedBrandingEnabled = useCallback((enabled) => {
|
|
134
|
+
try {
|
|
135
|
+
localStorage.setItem(brandingStorageKey, enabled === false ? "0" : "1");
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// ignore
|
|
139
|
+
}
|
|
140
|
+
}, [brandingStorageKey]);
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
setBrandingEnabled(readBrandingCache());
|
|
143
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
144
|
+
}, [brandingStorageKey]);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
lastPageviewRef.current = "";
|
|
147
|
+
}, [resolvedApiKey]);
|
|
148
|
+
const trackPageviewOnce = useCallback((path) => {
|
|
149
|
+
const next = (path || "").toString();
|
|
150
|
+
if (!next)
|
|
151
|
+
return;
|
|
152
|
+
if (lastPageviewRef.current === next)
|
|
153
|
+
return;
|
|
154
|
+
lastPageviewRef.current = next;
|
|
155
|
+
apiRef.current.trackPageview(next);
|
|
156
|
+
}, []);
|
|
113
157
|
const enablePrehide = useCallback((bgColor) => {
|
|
114
158
|
if (typeof document === "undefined")
|
|
115
159
|
return;
|
|
@@ -306,11 +350,19 @@ navigateRef, // For path mode routing
|
|
|
306
350
|
setEntitlements(bootstrap.entitlements);
|
|
307
351
|
if (bootstrap?.loading_bg_color)
|
|
308
352
|
setCachedLoadingBgColor(bootstrap.loading_bg_color);
|
|
353
|
+
if (bootstrap?.entitlements?.brandingRequired) {
|
|
354
|
+
setBrandingEnabled(true);
|
|
355
|
+
setCachedBrandingEnabled(true);
|
|
356
|
+
}
|
|
357
|
+
else if (typeof bootstrap?.branding_enabled === "boolean") {
|
|
358
|
+
setBrandingEnabled(bootstrap.branding_enabled);
|
|
359
|
+
setCachedBrandingEnabled(bootstrap.branding_enabled);
|
|
360
|
+
}
|
|
309
361
|
})();
|
|
310
362
|
return () => {
|
|
311
363
|
cancelled = true;
|
|
312
364
|
};
|
|
313
|
-
}, [defaultLocale, entitlements, locale, setCachedLoadingBgColor]);
|
|
365
|
+
}, [defaultLocale, entitlements, locale, setCachedBrandingEnabled, setCachedLoadingBgColor]);
|
|
314
366
|
const applySeoBundle = useCallback((bundle, hreflangEnabled) => {
|
|
315
367
|
try {
|
|
316
368
|
const head = document.head;
|
|
@@ -535,6 +587,14 @@ navigateRef, // For path mode routing
|
|
|
535
587
|
setCachedLoadingBgColor(bootstrap.loading_bg_color);
|
|
536
588
|
enablePrehide(bootstrap.loading_bg_color);
|
|
537
589
|
}
|
|
590
|
+
if ((bootstrap?.entitlements || nextEntitlements)?.brandingRequired) {
|
|
591
|
+
setBrandingEnabled(true);
|
|
592
|
+
setCachedBrandingEnabled(true);
|
|
593
|
+
}
|
|
594
|
+
else if (typeof bootstrap?.branding_enabled === "boolean") {
|
|
595
|
+
setBrandingEnabled(bootstrap.branding_enabled);
|
|
596
|
+
setCachedBrandingEnabled(bootstrap.branding_enabled);
|
|
597
|
+
}
|
|
538
598
|
const exclusions = Array.isArray(bootstrap?.exclusions)
|
|
539
599
|
? bootstrap.exclusions
|
|
540
600
|
.map((row) => {
|
|
@@ -630,18 +690,9 @@ navigateRef, // For path mode routing
|
|
|
630
690
|
toTranslations,
|
|
631
691
|
writeCriticalCache,
|
|
632
692
|
]);
|
|
633
|
-
// SPA router hook-in: track History API navigations (React Router/Next/etc) without app changes.
|
|
634
693
|
useEffect(() => {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const originalReplaceState = historyObj.replaceState.bind(historyObj);
|
|
638
|
-
const onNavigate = () => {
|
|
639
|
-
try {
|
|
640
|
-
apiRef.current.trackPageview(window.location.pathname + window.location.search);
|
|
641
|
-
}
|
|
642
|
-
catch {
|
|
643
|
-
// ignore
|
|
644
|
-
}
|
|
694
|
+
onNavigateRef.current = () => {
|
|
695
|
+
trackPageviewOnce(window.location.pathname + window.location.search);
|
|
645
696
|
const nextLocale = detectLocale();
|
|
646
697
|
if (nextLocale !== locale) {
|
|
647
698
|
setLocaleState(nextLocale);
|
|
@@ -651,25 +702,50 @@ navigateRef, // For path mode routing
|
|
|
651
702
|
applyActiveTranslations(document.body);
|
|
652
703
|
}
|
|
653
704
|
};
|
|
705
|
+
}, [defaultLocale, detectLocale, loadData, locale, mode, trackPageviewOnce]);
|
|
706
|
+
// SPA router hook-in: patch History API once (prevents stacked wrappers → request storms).
|
|
707
|
+
useEffect(() => {
|
|
708
|
+
if (typeof window === "undefined")
|
|
709
|
+
return;
|
|
710
|
+
if (historyPatchedRef.current)
|
|
711
|
+
return;
|
|
712
|
+
historyPatchedRef.current = true;
|
|
713
|
+
const historyObj = window.history;
|
|
714
|
+
const originalPushState = historyObj.pushState.bind(historyObj);
|
|
715
|
+
const originalReplaceState = historyObj.replaceState.bind(historyObj);
|
|
716
|
+
originalHistoryRef.current = { pushState: originalPushState, replaceState: originalReplaceState };
|
|
717
|
+
const safeOnNavigate = () => {
|
|
718
|
+
try {
|
|
719
|
+
onNavigateRef.current();
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
// ignore
|
|
723
|
+
}
|
|
724
|
+
};
|
|
654
725
|
historyObj.pushState = ((...args) => {
|
|
655
726
|
const ret = originalPushState(...args);
|
|
656
|
-
|
|
727
|
+
safeOnNavigate();
|
|
657
728
|
return ret;
|
|
658
729
|
});
|
|
659
730
|
historyObj.replaceState = ((...args) => {
|
|
660
731
|
const ret = originalReplaceState(...args);
|
|
661
|
-
|
|
732
|
+
safeOnNavigate();
|
|
662
733
|
return ret;
|
|
663
734
|
});
|
|
664
|
-
window.addEventListener("popstate",
|
|
665
|
-
window.addEventListener("hashchange",
|
|
735
|
+
window.addEventListener("popstate", safeOnNavigate);
|
|
736
|
+
window.addEventListener("hashchange", safeOnNavigate);
|
|
666
737
|
return () => {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
738
|
+
const originals = originalHistoryRef.current;
|
|
739
|
+
if (originals) {
|
|
740
|
+
historyObj.pushState = originals.pushState;
|
|
741
|
+
historyObj.replaceState = originals.replaceState;
|
|
742
|
+
}
|
|
743
|
+
window.removeEventListener("popstate", safeOnNavigate);
|
|
744
|
+
window.removeEventListener("hashchange", safeOnNavigate);
|
|
745
|
+
originalHistoryRef.current = null;
|
|
746
|
+
historyPatchedRef.current = false;
|
|
671
747
|
};
|
|
672
|
-
}, [
|
|
748
|
+
}, []);
|
|
673
749
|
// Change locale
|
|
674
750
|
const setLocale = useCallback((newLocale) => {
|
|
675
751
|
void (async () => {
|
|
@@ -726,7 +802,7 @@ navigateRef, // For path mode routing
|
|
|
726
802
|
useEffect(() => {
|
|
727
803
|
const initialLocale = detectLocale();
|
|
728
804
|
// Track initial page (fallback discovery for pages not present in the routes feed).
|
|
729
|
-
|
|
805
|
+
trackPageviewOnce(window.location.pathname + window.location.search);
|
|
730
806
|
// Always prefetch artifacts for the initial locale (pipeline-produced translations + rules).
|
|
731
807
|
loadData(initialLocale);
|
|
732
808
|
// Set up keyboard shortcut for edit mode
|
|
@@ -744,7 +820,7 @@ navigateRef, // For path mode routing
|
|
|
744
820
|
clearTimeout(retryTimeoutRef.current);
|
|
745
821
|
}
|
|
746
822
|
};
|
|
747
|
-
}, [detectLocale, loadData, editKey]);
|
|
823
|
+
}, [detectLocale, loadData, editKey, trackPageviewOnce]);
|
|
748
824
|
// Auto-inject sitemap link tag
|
|
749
825
|
useEffect(() => {
|
|
750
826
|
if (sitemap && resolvedApiKey && isSeoActive()) {
|
|
@@ -1032,7 +1108,9 @@ navigateRef, // For path mode routing
|
|
|
1032
1108
|
};
|
|
1033
1109
|
return (React.createElement(LovalingoContext.Provider, { value: contextValue },
|
|
1034
1110
|
children,
|
|
1035
|
-
React.createElement(LanguageSwitcher, { locales: allLocales, currentLocale: locale, onLocaleChange: setLocale, position: switcherPosition, offsetY: switcherOffsetY, theme: switcherTheme, branding:
|
|
1036
|
-
|
|
1037
|
-
:
|
|
1111
|
+
React.createElement(LanguageSwitcher, { locales: allLocales, currentLocale: locale, onLocaleChange: setLocale, position: switcherPosition, offsetY: switcherOffsetY, theme: switcherTheme, branding: {
|
|
1112
|
+
required: Boolean(entitlements?.brandingRequired),
|
|
1113
|
+
enabled: brandingEnabled,
|
|
1114
|
+
href: "https://lovalingo.com",
|
|
1115
|
+
} })));
|
|
1038
1116
|
};
|
|
@@ -205,7 +205,7 @@ export const LanguageSwitcher = ({ locales, currentLocale, onLocaleChange, posit
|
|
|
205
205
|
e.currentTarget.style.filter = 'brightness(1)';
|
|
206
206
|
e.currentTarget.style.transform = 'scale(1)';
|
|
207
207
|
}, "aria-label": `Switch to ${locale.toUpperCase()}`, title: locale.toUpperCase(), tabIndex: isOpen ? 0 : -1 }, LANGUAGE_FLAGS[locale] || '🏳️')))),
|
|
208
|
-
branding?.required && (React.createElement("div", { style: badgeRowStyles, "aria-label": "Lovalingo branding" },
|
|
208
|
+
(branding?.required || branding?.enabled) && (React.createElement("div", { style: badgeRowStyles, "aria-label": "Lovalingo branding" },
|
|
209
209
|
React.createElement("a", { href: branding.href || 'https://lovalingo.com', target: "_blank", rel: "noreferrer", style: badgeLinkStyles, tabIndex: isOpen ? 0 : -1, "aria-label": "Localized by Lovalingo", title: "Localized by Lovalingo" },
|
|
210
210
|
React.createElement("span", { style: {
|
|
211
211
|
width: '16px',
|
package/dist/utils/api.d.ts
CHANGED
package/package.json
CHANGED