@lovalingo/lovalingo 0.3.3 → 0.4.0

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/README.md CHANGED
@@ -31,6 +31,16 @@ npm install @lovalingo/lovalingo react-router-dom
31
31
 
32
32
  Pricing and onboarding: https://lovalingo.com
33
33
 
34
+ ## API endpoint (recommended default)
35
+
36
+ By default the runtime uses `https://cdn.lovalingo.com` (Cloudflare edge) for `bootstrap/bundle/dom-rules`.
37
+
38
+ You can override it (advanced):
39
+
40
+ ```tsx
41
+ <LovalingoProvider apiBase="https://<your-supabase-project>.supabase.co" ... />
42
+ ```
43
+
34
44
  ## React Router
35
45
 
36
46
  ### Query mode (default)
@@ -11,7 +11,7 @@ const LOCALE_STORAGE_KEY = 'Lovalingo_locale';
11
11
  const LOADING_BG_STORAGE_PREFIX = "Lovalingo_loading_bg_color";
12
12
  const BRANDING_STORAGE_PREFIX = "Lovalingo_branding_enabled";
13
13
  const CRITICAL_CACHE_PREFIX = "Lovalingo_critical_v0_3";
14
- export const LovalingoProvider = ({ children, apiKey: apiKeyProp, publicAnonKey, defaultLocale, locales, apiBase = 'https://leuskvkajliuzalrlwhw.supabase.co', routing = 'query', // Default to query mode (backward compatible)
14
+ export const LovalingoProvider = ({ children, apiKey: apiKeyProp, publicAnonKey, defaultLocale, locales, apiBase = 'https://cdn.lovalingo.com', routing = 'query', // Default to query mode (backward compatible)
15
15
  autoPrefixLinks = true, autoApplyRules = true, switcherPosition = 'bottom-right', switcherOffsetY = 20, switcherTheme = 'dark', editMode: initialEditMode = false, editKey = 'KeyE', pathNormalization = { enabled: true }, // Enable by default
16
16
  mode = 'dom', // Default to legacy DOM mode for backward compatibility
17
17
  sitemap = true, // Default: true - Auto-inject sitemap link tag
@@ -74,6 +74,10 @@ navigateRef, // For path mode routing
74
74
  apiRef.current = new LovalingoAPI(resolvedApiKey, apiBase, enhancedPathConfig);
75
75
  }, [apiBase, enhancedPathConfig, resolvedApiKey]);
76
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);
77
81
  const retryTimeoutRef = useRef(null);
78
82
  const isNavigatingRef = useRef(false);
79
83
  const isInternalNavigationRef = useRef(false);
@@ -138,6 +142,18 @@ navigateRef, // For path mode routing
138
142
  setBrandingEnabled(readBrandingCache());
139
143
  // eslint-disable-next-line react-hooks/exhaustive-deps
140
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
+ }, []);
141
157
  const enablePrehide = useCallback((bgColor) => {
142
158
  if (typeof document === "undefined")
143
159
  return;
@@ -674,18 +690,9 @@ navigateRef, // For path mode routing
674
690
  toTranslations,
675
691
  writeCriticalCache,
676
692
  ]);
677
- // SPA router hook-in: track History API navigations (React Router/Next/etc) without app changes.
678
693
  useEffect(() => {
679
- const historyObj = window.history;
680
- const originalPushState = historyObj.pushState.bind(historyObj);
681
- const originalReplaceState = historyObj.replaceState.bind(historyObj);
682
- const onNavigate = () => {
683
- try {
684
- apiRef.current.trackPageview(window.location.pathname + window.location.search);
685
- }
686
- catch {
687
- // ignore
688
- }
694
+ onNavigateRef.current = () => {
695
+ trackPageviewOnce(window.location.pathname + window.location.search);
689
696
  const nextLocale = detectLocale();
690
697
  if (nextLocale !== locale) {
691
698
  setLocaleState(nextLocale);
@@ -695,25 +702,50 @@ navigateRef, // For path mode routing
695
702
  applyActiveTranslations(document.body);
696
703
  }
697
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
+ };
698
725
  historyObj.pushState = ((...args) => {
699
726
  const ret = originalPushState(...args);
700
- onNavigate();
727
+ safeOnNavigate();
701
728
  return ret;
702
729
  });
703
730
  historyObj.replaceState = ((...args) => {
704
731
  const ret = originalReplaceState(...args);
705
- onNavigate();
732
+ safeOnNavigate();
706
733
  return ret;
707
734
  });
708
- window.addEventListener("popstate", onNavigate);
709
- window.addEventListener("hashchange", onNavigate);
735
+ window.addEventListener("popstate", safeOnNavigate);
736
+ window.addEventListener("hashchange", safeOnNavigate);
710
737
  return () => {
711
- historyObj.pushState = originalPushState;
712
- historyObj.replaceState = originalReplaceState;
713
- window.removeEventListener("popstate", onNavigate);
714
- window.removeEventListener("hashchange", onNavigate);
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;
715
747
  };
716
- }, [defaultLocale, detectLocale, loadData, locale, mode]);
748
+ }, []);
717
749
  // Change locale
718
750
  const setLocale = useCallback((newLocale) => {
719
751
  void (async () => {
@@ -770,7 +802,7 @@ navigateRef, // For path mode routing
770
802
  useEffect(() => {
771
803
  const initialLocale = detectLocale();
772
804
  // Track initial page (fallback discovery for pages not present in the routes feed).
773
- apiRef.current.trackPageview(window.location.pathname + window.location.search);
805
+ trackPageviewOnce(window.location.pathname + window.location.search);
774
806
  // Always prefetch artifacts for the initial locale (pipeline-produced translations + rules).
775
807
  loadData(initialLocale);
776
808
  // Set up keyboard shortcut for edit mode
@@ -788,7 +820,7 @@ navigateRef, // For path mode routing
788
820
  clearTimeout(retryTimeoutRef.current);
789
821
  }
790
822
  };
791
- }, [detectLocale, loadData, editKey]);
823
+ }, [detectLocale, loadData, editKey, trackPageviewOnce]);
792
824
  // Auto-inject sitemap link tag
793
825
  useEffect(() => {
794
826
  if (sitemap && resolvedApiKey && isSeoActive()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovalingo/lovalingo",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
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",