@lovalingo/lovalingo 0.5.3 → 0.5.5

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/LICENSE CHANGED
@@ -1,9 +1,9 @@
1
1
  LOVALINGO COMMERCIAL LICENSE
2
2
 
3
- Copyright (c) 2025 Mertens Advies. All rights reserved.
3
+ Copyright (c) 2026 Lovalingo Swiss. All rights reserved.
4
4
 
5
5
  NOTICE: This software and associated documentation files (the "Software") are the
6
- proprietary and confidential information of Mertens Advies.
6
+ proprietary and confidential information of Lovalingo Swiss.
7
7
 
8
8
  The Software is licensed, not sold. This license grants you the following rights:
9
9
 
@@ -74,17 +74,17 @@ FOR END CLIENTS:
74
74
  4. API KEY REQUIREMENT
75
75
  ====================================================================================
76
76
 
77
- Production use of the Software requires a valid API key obtained from Mertens Advies.
77
+ Production use of the Software requires a valid API key obtained from Lovalingo Swiss.
78
78
  Usage is subject to the terms of service associated with your API key subscription.
79
79
 
80
80
  ====================================================================================
81
81
  5. INTELLECTUAL PROPERTY
82
82
  ====================================================================================
83
83
 
84
- This Software is the exclusive intellectual property of Mertens Advies.
84
+ This Software is the exclusive intellectual property of Lovalingo Swiss.
85
85
  All rights, title, and interest in and to the Software, including all
86
86
  intellectual property rights, patents, trademarks, copyrights, and trade secrets,
87
- remain the sole property of Mertens Advies.
87
+ remain the sole property of Lovalingo Swiss.
88
88
 
89
89
  ====================================================================================
90
90
  6. NOT OPEN SOURCE
@@ -133,16 +133,16 @@ SUCH DAMAGE.
133
133
  ====================================================================================
134
134
 
135
135
  This license shall be governed by and construed in accordance with the laws of
136
- the jurisdiction in which Mertens Advies operates, without regard to its conflict
136
+ the jurisdiction in which Lovalingo Swiss operates, without regard to its conflict
137
137
  of law provisions.
138
138
 
139
139
  ====================================================================================
140
140
 
141
141
  For licensing inquiries, enterprise licensing, or questions about permitted use,
142
- please contact Mertens Advies.
142
+ please contact Lovalingo Swiss.
143
143
 
144
144
  MERTENS ADVIES
145
145
  Lovalingo Translation Platform
146
- © 2025 All Rights Reserved
146
+ © 2026 All Rights Reserved
147
147
 
148
- Website: [Contact Mertens Advies for licensing information]
148
+ Website: [Contact Lovalingo Swiss for licensing information]
package/README.md CHANGED
@@ -21,7 +21,10 @@ The runtime now marks translatable text nodes deterministically and exposes mark
21
21
  - Coverage is enforced server-side (jobs fail if marker coverage is below the threshold).
22
22
  - This is a breaking change: older runtimes will be rejected by the pipeline.
23
23
 
24
- Debug (runtime logs): set `window.__lovalingoDebug = true` before initializing `LovalingoProvider`.
24
+ Debug (runtime logs):
25
+ - append `?lovalingoDebug=1` to the URL (works across reloads)
26
+ - or run `localStorage.setItem("Lovalingo_debug","1")` and reload
27
+ - or set `window.__lovalingoDebug = true` (same-tab only; not persistent)
25
28
 
26
29
  ## Installation
27
30
 
@@ -137,7 +140,7 @@ You still need to serve `/sitemap.xml` on your own domain (recommended: reverse-
137
140
 
138
141
  ## License
139
142
 
140
- Commercial license (not open source). See `react-package/LICENSE`.
143
+ Commercial license (not open source). See `LICENSE`.
141
144
 
142
145
  Manual translation control:
143
146
 
@@ -349,7 +352,7 @@ export async function GET() {
349
352
 
350
353
  **COMMERCIAL LICENSE - NOT OPEN SOURCE**
351
354
 
352
- Copyright (c) 2025 Mertens Advies. All rights reserved.
355
+ Copyright (c) 2026 Lovalingo Swiss. All rights reserved.
353
356
 
354
357
  ### For Agencies & Developers
355
358
 
@@ -374,6 +377,6 @@ applications containing Lovalingo, but may not modify, redistribute, or extract
374
377
 
375
378
  This software is licensed under the **Lovalingo Commercial License**.
376
379
  This is NOT open source software. All intellectual property rights remain the
377
- exclusive property of Mertens Advies.
380
+ exclusive property of Lovalingo Swiss.
378
381
 
379
382
  See LICENSE file for complete terms and conditions.
@@ -114,6 +114,7 @@ navigateRef, // For path mode routing
114
114
  const prehideStateRef = useRef({
115
115
  active: false,
116
116
  timeoutId: null,
117
+ startedAtMs: null,
117
118
  prevHtmlVisibility: "",
118
119
  prevBodyVisibility: "",
119
120
  prevHtmlBg: "",
@@ -198,6 +199,27 @@ navigateRef, // For path mode routing
198
199
  pageviewFingerprintTimeoutRef.current = window.setTimeout(trySendFingerprint, 800);
199
200
  pageviewFingerprintRetryTimeoutRef.current = window.setTimeout(trySendFingerprint, 2000);
200
201
  }, []);
202
+ const forceDisablePrehide = useCallback(() => {
203
+ if (typeof document === "undefined")
204
+ return;
205
+ const html = document.documentElement;
206
+ const body = document.body;
207
+ if (!html || !body)
208
+ return;
209
+ const state = prehideStateRef.current;
210
+ if (state.timeoutId != null) {
211
+ window.clearTimeout(state.timeoutId);
212
+ state.timeoutId = null;
213
+ }
214
+ if (!state.active)
215
+ return;
216
+ state.active = false;
217
+ state.startedAtMs = null;
218
+ html.style.visibility = state.prevHtmlVisibility;
219
+ body.style.visibility = state.prevBodyVisibility;
220
+ html.style.backgroundColor = state.prevHtmlBg;
221
+ body.style.backgroundColor = state.prevBodyBg;
222
+ }, []);
201
223
  const enablePrehide = useCallback((bgColor) => {
202
224
  if (typeof document === "undefined")
203
225
  return;
@@ -206,8 +228,13 @@ navigateRef, // For path mode routing
206
228
  if (!html || !body)
207
229
  return;
208
230
  const state = prehideStateRef.current;
231
+ // Why: avoid "perma-hidden" pages when repeated navigation/errors keep prehide active; always hard-stop after a few seconds.
232
+ if (state.active && state.startedAtMs != null && Date.now() - state.startedAtMs > PREHIDE_FAILSAFE_MS * 3) {
233
+ forceDisablePrehide();
234
+ }
209
235
  if (!state.active) {
210
236
  state.active = true;
237
+ state.startedAtMs = Date.now();
211
238
  state.prevHtmlVisibility = html.style.visibility || "";
212
239
  state.prevBodyVisibility = body.style.visibility || "";
213
240
  state.prevHtmlBg = html.style.backgroundColor || "";
@@ -223,28 +250,9 @@ navigateRef, // For path mode routing
223
250
  return;
224
251
  }
225
252
  // Why: avoid a "perma-hide" when navigation events repeatedly re-trigger prehide and keep extending the timeout.
226
- state.timeoutId = window.setTimeout(() => disablePrehide(), PREHIDE_FAILSAFE_MS);
227
- }, []);
228
- const disablePrehide = useCallback(() => {
229
- if (typeof document === "undefined")
230
- return;
231
- const html = document.documentElement;
232
- const body = document.body;
233
- if (!html || !body)
234
- return;
235
- const state = prehideStateRef.current;
236
- if (state.timeoutId != null) {
237
- window.clearTimeout(state.timeoutId);
238
- state.timeoutId = null;
239
- }
240
- if (!state.active)
241
- return;
242
- state.active = false;
243
- html.style.visibility = state.prevHtmlVisibility;
244
- body.style.visibility = state.prevBodyVisibility;
245
- html.style.backgroundColor = state.prevHtmlBg;
246
- body.style.backgroundColor = state.prevBodyBg;
247
- }, []);
253
+ state.timeoutId = window.setTimeout(() => forceDisablePrehide(), PREHIDE_FAILSAFE_MS);
254
+ }, [forceDisablePrehide]);
255
+ const disablePrehide = forceDisablePrehide;
248
256
  const buildCriticalCacheKey = useCallback((targetLocale, normalizedPath) => {
249
257
  const key = `${resolvedApiKey || "anonymous"}:${targetLocale}:${normalizedPath || "/"}`;
250
258
  return `${CRITICAL_CACHE_PREFIX}:${hashContent(key)}`;
@@ -633,6 +641,7 @@ navigateRef, // For path mode routing
633
641
  if (previousLocale && previousLocale !== defaultLocale) {
634
642
  logDebug(`[Lovalingo] Switching from ${previousLocale} to ${targetLocale}`);
635
643
  }
644
+ let revealedViaCachedCritical = false;
636
645
  const cachedCritical = readCriticalCache(targetLocale, normalizedPath);
637
646
  if (cachedCritical?.loading_bg_color) {
638
647
  setCachedLoadingBgColor(cachedCritical.loading_bg_color);
@@ -648,6 +657,7 @@ navigateRef, // For path mode routing
648
657
  applyActiveTranslations(document.body);
649
658
  }
650
659
  disablePrehide();
660
+ revealedViaCachedCritical = true;
651
661
  }
652
662
  const bootstrap = await apiRef.current.fetchBootstrap(targetLocale, currentPath);
653
663
  const nextEntitlements = bootstrap?.entitlements || apiRef.current.getEntitlements();
@@ -686,6 +696,7 @@ navigateRef, // For path mode routing
686
696
  const criticalMap = bootstrap?.critical?.map && typeof bootstrap.critical.map === "object" && !Array.isArray(bootstrap.critical.map)
687
697
  ? bootstrap.critical.map
688
698
  : {};
699
+ const hasBootstrapCritical = Object.keys(criticalMap).length > 0;
689
700
  if (Object.keys(criticalMap).length > 0) {
690
701
  setActiveTranslations(toTranslations(criticalMap, targetLocale));
691
702
  if (mode === "dom") {
@@ -706,6 +717,37 @@ navigateRef, // For path mode routing
706
717
  exclusions,
707
718
  loading_bg_color: bootstrap?.loading_bg_color && /^#[0-9a-fA-F]{6}$/.test(bootstrap.loading_bg_color) ? bootstrap.loading_bg_color : null,
708
719
  });
720
+ const shouldWaitForBundle = !revealedViaCachedCritical && !hasBootstrapCritical;
721
+ if (shouldWaitForBundle) {
722
+ // Why: if there's no critical slice for first paint, wait for the bundle (within the prehide failsafe) to avoid a visible flash.
723
+ const bundle = await apiRef.current.fetchBundle(targetLocale, currentPath);
724
+ if (bundle?.map && typeof bundle.map === "object") {
725
+ const translations = toTranslations(bundle.map, targetLocale);
726
+ if (translations.length > 0) {
727
+ translationCacheRef.current.set(cacheKey, { translations });
728
+ setActiveTranslations(translations);
729
+ if (mode === "dom") {
730
+ applyActiveTranslations(document.body);
731
+ }
732
+ }
733
+ }
734
+ }
735
+ else {
736
+ // Lazy-load the full page bundle after first paint.
737
+ void (async () => {
738
+ const bundle = await apiRef.current.fetchBundle(targetLocale, currentPath);
739
+ if (!bundle || !bundle.map)
740
+ return;
741
+ const translations = toTranslations(bundle.map, targetLocale);
742
+ if (translations.length === 0)
743
+ return;
744
+ translationCacheRef.current.set(cacheKey, { translations });
745
+ setActiveTranslations(translations);
746
+ if (mode === "dom") {
747
+ applyActiveTranslations(document.body);
748
+ }
749
+ })();
750
+ }
709
751
  disablePrehide();
710
752
  // Delayed retry scan to catch late-rendering content
711
753
  retryTimeoutRef.current = setTimeout(() => {
@@ -722,20 +764,6 @@ navigateRef, // For path mode routing
722
764
  applyDomRules(rules);
723
765
  }
724
766
  }, 500);
725
- // Lazy-load the full page bundle after first paint.
726
- void (async () => {
727
- const bundle = await apiRef.current.fetchBundle(targetLocale, currentPath);
728
- if (!bundle || !bundle.map)
729
- return;
730
- const translations = toTranslations(bundle.map, targetLocale);
731
- if (translations.length === 0)
732
- return;
733
- translationCacheRef.current.set(cacheKey, { translations });
734
- setActiveTranslations(translations);
735
- if (mode === "dom") {
736
- applyActiveTranslations(document.body);
737
- }
738
- })();
739
767
  }
740
768
  catch (error) {
741
769
  errorDebug('Error loading translations:', error);
@@ -754,6 +782,7 @@ navigateRef, // For path mode routing
754
782
  }
755
783
  }, [
756
784
  applySeoBundle,
785
+ allLocales,
757
786
  autoApplyRules,
758
787
  defaultLocale,
759
788
  disablePrehide,
@@ -763,6 +792,8 @@ navigateRef, // For path mode routing
763
792
  isSeoActive,
764
793
  mode,
765
794
  readCriticalCache,
795
+ routing,
796
+ routingConfig.nonLocalizedPaths,
766
797
  setCachedLoadingBgColor,
767
798
  toTranslations,
768
799
  writeCriticalCache,
@@ -890,16 +921,25 @@ navigateRef, // For path mode routing
890
921
  })().finally(() => {
891
922
  isInternalNavigationRef.current = false;
892
923
  });
893
- }, [allLocales, locale, routing, loadData, navigateRef, routingConfig.nonLocalizedPaths]);
924
+ }, [allLocales, defaultLocale, locale, routing, loadData, navigateRef, routingConfig.nonLocalizedPaths]);
894
925
  // No string-level "misses" from the client: the pipeline (Browser Rendering) is source-of-truth for string discovery.
926
+ // Why: prevent init/load effects from re-running (and calling bootstrap/bundle again) when loadData changes due to state updates.
927
+ const loadDataRef = useRef(loadData);
928
+ useEffect(() => {
929
+ loadDataRef.current = loadData;
930
+ }, [loadData]);
931
+ const detectLocaleRef = useRef(detectLocale);
932
+ useEffect(() => {
933
+ detectLocaleRef.current = detectLocale;
934
+ }, [detectLocale]);
895
935
  // Initialize
896
936
  useEffect(() => {
897
- const initialLocale = detectLocale();
937
+ const initialLocale = detectLocaleRef.current();
898
938
  lastNormalizedPathRef.current = processPath(window.location.pathname, enhancedPathConfig);
899
939
  // Track initial page (fallback discovery for pages not present in the routes feed).
900
940
  trackPageviewOnce(window.location.pathname + window.location.search);
901
941
  // Always prefetch artifacts for the initial locale (pipeline-produced translations + rules).
902
- loadData(initialLocale);
942
+ loadDataRef.current(initialLocale);
903
943
  // Set up keyboard shortcut for edit mode
904
944
  const handleKeyPress = (e) => {
905
945
  if (e.code === editKey && (e.ctrlKey || e.metaKey)) {
@@ -915,7 +955,7 @@ navigateRef, // For path mode routing
915
955
  clearTimeout(retryTimeoutRef.current);
916
956
  }
917
957
  };
918
- }, [detectLocale, enhancedPathConfig, loadData, editKey, trackPageviewOnce]);
958
+ }, [editKey, enhancedPathConfig, trackPageviewOnce]);
919
959
  // Auto-inject sitemap link tag
920
960
  useEffect(() => {
921
961
  if (sitemap && resolvedApiKey && isSeoActive()) {
@@ -29,7 +29,9 @@ export function LangLink({ to, ...props }) {
29
29
  ? (() => {
30
30
  const trimmed = (to || '').toString().trim();
31
31
  const normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
32
- return isNonLocalizedPath(normalized, routing.nonLocalizedPaths) ? normalized : `/${lang}${normalized}`;
32
+ return isNonLocalizedPath(normalized, routing.nonLocalizedPaths)
33
+ ? normalized
34
+ : `/${lang}${normalized}`;
33
35
  })()
34
36
  : to;
35
37
  return React.createElement(Link, { ...props, to: langTo });
@@ -1,8 +1,8 @@
1
1
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
- import { BrowserRouter, Routes, Route, Outlet, useLocation, useNavigate } from 'react-router-dom';
2
+ import { BrowserRouter, Routes, Route, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
3
3
  import { LangContext } from '../context/LangContext';
4
4
  import { LangRoutingContext } from '../context/LangRoutingContext';
5
- import { isNonLocalizedPath, parseBootstrapNonLocalizedPaths } from '../utils/nonLocalizedPaths';
5
+ import { isNonLocalizedPath, parseBootstrapInactivePages, parseBootstrapNonLocalizedPaths } from '../utils/nonLocalizedPaths';
6
6
  import { logDebug } from '../utils/logger';
7
7
  /**
8
8
  * NavigateExporter - Internal component that exports navigate function via ref
@@ -19,20 +19,17 @@ function NavigateExporter({ navigateRef }) {
19
19
  /**
20
20
  * LangGuard - Internal component that validates language and provides it to children
21
21
  */
22
- function LangGuard({ lang, nonLocalizedPaths, }) {
22
+ function LangGuard({ lang, nonLocalizedPaths, defaultLang, }) {
23
23
  const location = useLocation();
24
- const navigate = useNavigate();
25
24
  // If the URL is language-prefixed but the underlying route is non-localized (e.g. robots/sitemap),
26
25
  // redirect to the canonical non-localized path.
27
26
  const prefix = `/${lang}`;
28
27
  const restPath = location.pathname.startsWith(prefix) ? location.pathname.slice(prefix.length) || '/' : location.pathname;
29
- const shouldDePrefix = isNonLocalizedPath(restPath, nonLocalizedPaths);
30
- useEffect(() => {
31
- if (!shouldDePrefix)
32
- return;
28
+ // Why: only explicit non-localized rules may strip the locale prefix; inactive pages must never affect client routing.
29
+ if (isNonLocalizedPath(restPath, nonLocalizedPaths)) {
33
30
  const nextPath = `${restPath}${location.search}${location.hash}`;
34
- navigate(nextPath, { replace: true });
35
- }, [location.hash, location.search, navigate, restPath, shouldDePrefix]);
31
+ return React.createElement(Navigate, { to: nextPath, replace: true });
32
+ }
36
33
  // Valid language - render children (user's routes)
37
34
  return (React.createElement(LangContext.Provider, { value: lang },
38
35
  React.createElement(Outlet, { context: { lang } })));
@@ -95,6 +92,7 @@ export function LangRouter({ children, defaultLang, langs, navigateRef, apiBase,
95
92
  ? apiBase.trim()
96
93
  : "https://cdn.lovalingo.com");
97
94
  const nonLocalizedStorageKey = useMemo(() => `Lovalingo_non_localized_paths:${resolvedApiKey || "anonymous"}`, [resolvedApiKey]);
95
+ const inactivePagesStorageKey = useMemo(() => `Lovalingo_inactive_pages:${resolvedApiKey || "anonymous"}`, [resolvedApiKey]);
98
96
  const [nonLocalizedPaths, setNonLocalizedPaths] = useState(() => {
99
97
  if (typeof window === "undefined")
100
98
  return [];
@@ -111,12 +109,28 @@ export function LangRouter({ children, defaultLang, langs, navigateRef, apiBase,
111
109
  return [];
112
110
  }
113
111
  });
112
+ const [inactivePages, setInactivePages] = useState(() => {
113
+ if (typeof window === "undefined")
114
+ return [];
115
+ if (!resolvedApiKey)
116
+ return [];
117
+ try {
118
+ const raw = localStorage.getItem(inactivePagesStorageKey);
119
+ if (!raw)
120
+ return [];
121
+ const parsed = JSON.parse(raw);
122
+ return parseBootstrapInactivePages(parsed);
123
+ }
124
+ catch {
125
+ return [];
126
+ }
127
+ });
114
128
  const [routingStatus, setRoutingStatus] = useState(() => {
115
129
  if (!resolvedApiKey)
116
130
  return "unknown";
117
- return nonLocalizedPaths.length > 0 ? "ready" : "loading";
131
+ return nonLocalizedPaths.length > 0 || inactivePages.length > 0 ? "ready" : "loading";
118
132
  });
119
- const fetchNonLocalizedPaths = useCallback(async () => {
133
+ const fetchRoutingConfig = useCallback(async () => {
120
134
  if (typeof window === "undefined")
121
135
  return;
122
136
  if (!resolvedApiKey)
@@ -129,7 +143,10 @@ export function LangRouter({ children, defaultLang, langs, navigateRef, apiBase,
129
143
  throw new Error(`bootstrap HTTP ${resolvedResponse.status}`);
130
144
  const data = (await resolvedResponse.json());
131
145
  const record = (data || {});
132
- return parseBootstrapNonLocalizedPaths(record["non_localized_paths"]);
146
+ return {
147
+ nonLocalizedPaths: parseBootstrapNonLocalizedPaths(record["non_localized_paths"]),
148
+ inactivePages: parseBootstrapInactivePages(record["inactive_pages"]),
149
+ };
133
150
  }, [defaultLang, resolvedApiBase, resolvedApiKey]);
134
151
  useEffect(() => {
135
152
  let cancelled = false;
@@ -138,13 +155,15 @@ export function LangRouter({ children, defaultLang, langs, navigateRef, apiBase,
138
155
  return;
139
156
  setRoutingStatus((prev) => (prev === "ready" ? prev : "loading"));
140
157
  try {
141
- const next = await fetchNonLocalizedPaths();
158
+ const next = await fetchRoutingConfig();
142
159
  if (cancelled || !next)
143
160
  return;
144
- setNonLocalizedPaths(next);
161
+ setNonLocalizedPaths(next.nonLocalizedPaths);
162
+ setInactivePages(next.inactivePages);
145
163
  setRoutingStatus("ready");
146
164
  try {
147
- localStorage.setItem(nonLocalizedStorageKey, JSON.stringify(next));
165
+ localStorage.setItem(nonLocalizedStorageKey, JSON.stringify(next.nonLocalizedPaths));
166
+ localStorage.setItem(inactivePagesStorageKey, JSON.stringify(next.inactivePages));
148
167
  }
149
168
  catch {
150
169
  // ignore
@@ -154,18 +173,18 @@ export function LangRouter({ children, defaultLang, langs, navigateRef, apiBase,
154
173
  if (cancelled)
155
174
  return;
156
175
  setRoutingStatus("error");
157
- logDebug("[Lovalingo] Failed to fetch non-localized paths:", err);
176
+ logDebug("[Lovalingo] Failed to fetch routing config:", err);
158
177
  }
159
178
  })();
160
179
  return () => {
161
180
  cancelled = true;
162
181
  };
163
- }, [fetchNonLocalizedPaths, nonLocalizedStorageKey, resolvedApiKey]);
182
+ }, [fetchRoutingConfig, inactivePagesStorageKey, nonLocalizedStorageKey, resolvedApiKey]);
164
183
  return (React.createElement(BrowserRouter, null,
165
184
  React.createElement(NavigateExporter, { navigateRef: navigateRef }),
166
- React.createElement(LangRoutingContext.Provider, { value: { nonLocalizedPaths, status: routingStatus } },
185
+ React.createElement(LangRoutingContext.Provider, { value: { defaultLang, nonLocalizedPaths, inactivePages, status: routingStatus } },
167
186
  React.createElement(Routes, null,
168
- langs.map((lang) => (React.createElement(Route, { key: lang, path: `${lang}/*`, element: React.createElement(LangGuard, { lang: lang, nonLocalizedPaths: nonLocalizedPaths }) },
187
+ langs.map((lang) => (React.createElement(Route, { key: lang, path: `${lang}/*`, element: React.createElement(LangGuard, { lang: lang, nonLocalizedPaths: nonLocalizedPaths, defaultLang: defaultLang }) },
169
188
  React.createElement(Route, { index: true, element: React.createElement(React.Fragment, null, children) }),
170
189
  React.createElement(Route, { path: "*", element: React.createElement(React.Fragment, null, children) })))),
171
190
  React.createElement(Route, { path: "*", element: React.createElement(RedirectToDefaultLang, { defaultLang: defaultLang, nonLocalizedPaths: nonLocalizedPaths, routingStatus: routingStatus }, children) })))));
@@ -210,16 +210,18 @@ export const LanguageSwitcher = ({ locales, currentLocale, onLocaleChange, posit
210
210
  React.createElement("span", { style: {
211
211
  width: '16px',
212
212
  height: '16px',
213
- borderRadius: '5px',
214
- background: '#6BD63D',
213
+ borderRadius: '999px',
214
+ overflow: 'hidden',
215
+ background: '#DA2576',
215
216
  display: 'inline-flex',
216
217
  alignItems: 'center',
217
218
  justifyContent: 'center',
218
219
  boxShadow: 'inset 0 0 0 1px rgba(0,0,0,0.25)',
219
220
  flexShrink: 0,
220
221
  } },
221
- React.createElement("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true" },
222
- React.createElement("path", { d: "M9 5a2 2 0 0 1 2 2v10h8a2 2 0 1 1 0 4H9a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Z", fill: "#0D0D0D" }))),
222
+ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 512 512", fill: "none", "aria-hidden": "true" },
223
+ React.createElement("path", { d: "M215.657 429.489C270.707 422.73 289.644 339.333 278 244.5C266.356 149.667 228.54 79.3089 173.49 86.0682C118.44 92.8275 83.253 175.184 94.8971 270.017C106.541 364.85 160.607 436.248 215.657 429.489Z", stroke: "#FFFFFF", strokeWidth: "44", strokeLinecap: "round", strokeLinejoin: "round" }),
224
+ React.createElement("path", { d: "M168.218 408.885C188.661 447.333 263.959 447.277 336.399 408.759C408.84 370.242 450.992 307.849 430.549 269.401C410.106 230.953 334.808 231.009 262.368 269.526C189.927 308.044 147.775 370.437 168.218 408.885Z", stroke: "#FFFFFF", strokeWidth: "44", strokeLinecap: "round", strokeLinejoin: "round" }))),
223
225
  React.createElement("span", null,
224
226
  branding.label || 'Localized by',
225
227
  " ",
@@ -1,6 +1,8 @@
1
1
  import type { NonLocalizedPathRule } from "../utils/nonLocalizedPaths";
2
2
  export type LangRoutingContextValue = {
3
+ defaultLang: string;
3
4
  nonLocalizedPaths: NonLocalizedPathRule[];
5
+ inactivePages: string[];
4
6
  status: "unknown" | "loading" | "ready" | "error";
5
7
  };
6
8
  export declare const LangRoutingContext: import("react").Context<LangRoutingContextValue>;
@@ -1,5 +1,7 @@
1
1
  import { createContext } from "react";
2
2
  export const LangRoutingContext = createContext({
3
+ defaultLang: "",
3
4
  nonLocalizedPaths: [],
5
+ inactivePages: [],
4
6
  status: "unknown",
5
7
  });
@@ -32,7 +32,9 @@ export function useLangNavigate() {
32
32
  if (!trimmed)
33
33
  return;
34
34
  const normalized = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
35
- const fullPath = isNonLocalizedPath(normalized, routing.nonLocalizedPaths) ? normalized : `/${lang}${normalized}`;
35
+ const fullPath = isNonLocalizedPath(normalized, routing.nonLocalizedPaths)
36
+ ? normalized
37
+ : `/${lang}${normalized}`;
36
38
  navigate(fullPath, options);
37
39
  }, [lang, navigate, routing.nonLocalizedPaths]);
38
40
  }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @Lovalingo/Lovalingo - Proprietary Translation Library
3
3
  *
4
- * Copyright (c) 2025 Mertens Advies. All rights reserved.
4
+ * Copyright (c) 2026 Lovalingo Swiss. All rights reserved.
5
5
  *
6
- * This software is the intellectual property of Mertens Advies.
6
+ * This software is the intellectual property of Lovalingo Swiss.
7
7
  * NOT OPEN SOURCE - All rights reserved.
8
8
  *
9
9
  * Unauthorized copying, modification, distribution, or use of this software
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @Lovalingo/Lovalingo - Proprietary Translation Library
3
3
  *
4
- * Copyright (c) 2025 Mertens Advies. All rights reserved.
4
+ * Copyright (c) 2026 Lovalingo Swiss. All rights reserved.
5
5
  *
6
- * This software is the intellectual property of Mertens Advies.
6
+ * This software is the intellectual property of Lovalingo Swiss.
7
7
  * NOT OPEN SOURCE - All rights reserved.
8
8
  *
9
9
  * Unauthorized copying, modification, distribution, or use of this software
@@ -30,6 +30,10 @@ export type BootstrapResponse = {
30
30
  match_type?: string;
31
31
  updated_at?: string | null;
32
32
  }>;
33
+ inactive_pages?: Array<{
34
+ page_path?: string;
35
+ updated_at?: string | null;
36
+ }>;
33
37
  loading_bg_color?: string | null;
34
38
  branding_enabled?: boolean;
35
39
  seoEnabled?: boolean;
@@ -4,6 +4,23 @@ function isDebugEnabled() {
4
4
  const value = globalThis.__lovalingoDebug;
5
5
  if (value === true || value === "true" || value === 1)
6
6
  return true;
7
+ try {
8
+ const params = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : null;
9
+ const query = params?.get("lovalingoDebug") || params?.get("lovalingo_debug") || "";
10
+ if (query === "1" || query === "true")
11
+ return true;
12
+ }
13
+ catch {
14
+ // ignore
15
+ }
16
+ try {
17
+ const stored = typeof window !== "undefined" ? window.localStorage?.getItem("Lovalingo_debug") : null;
18
+ if (stored === "1" || stored === "true")
19
+ return true;
20
+ }
21
+ catch {
22
+ // ignore
23
+ }
7
24
  return false;
8
25
  }
9
26
  export function logDebug(...args) {
@@ -7,3 +7,6 @@ export declare function matchesNonLocalizedRules(pathname: string, rules: NonLoc
7
7
  export declare function isNonLocalizedPath(pathname: string, rules: NonLocalizedPathRule[]): boolean;
8
8
  export declare function stripLocalePrefix(pathname: string, locales: string[]): string;
9
9
  export declare function parseBootstrapNonLocalizedPaths(value: unknown): NonLocalizedPathRule[];
10
+ export declare function parseBootstrapInactivePages(value: unknown): string[];
11
+ export declare function matchesRouteTemplate(pathname: string, template: string): boolean;
12
+ export declare function isInactivePagePath(pathname: string, inactivePages: string[]): boolean;
@@ -26,7 +26,12 @@ export function matchesNonLocalizedRules(pathname, rules) {
26
26
  continue;
27
27
  }
28
28
  if (matchType === "prefix") {
29
- if (input.startsWith(pattern))
29
+ if (input === pattern)
30
+ return true;
31
+ const normalizedPrefix = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
32
+ if (!normalizedPrefix || normalizedPrefix === "/")
33
+ return true;
34
+ if (input.startsWith(`${normalizedPrefix}/`))
30
35
  return true;
31
36
  continue;
32
37
  }
@@ -76,3 +81,56 @@ export function parseBootstrapNonLocalizedPaths(value) {
76
81
  }
77
82
  return out;
78
83
  }
84
+ export function parseBootstrapInactivePages(value) {
85
+ if (!Array.isArray(value))
86
+ return [];
87
+ const out = [];
88
+ for (const row of value) {
89
+ if (typeof row === "string") {
90
+ const pagePath = row.trim();
91
+ if (pagePath && pagePath.startsWith("/"))
92
+ out.push(pagePath);
93
+ continue;
94
+ }
95
+ if (!row || typeof row !== "object")
96
+ continue;
97
+ const record = row;
98
+ const pagePath = typeof record.page_path === "string" ? record.page_path.trim() : "";
99
+ if (!pagePath || !pagePath.startsWith("/"))
100
+ continue;
101
+ out.push(pagePath);
102
+ }
103
+ return out;
104
+ }
105
+ function escapeRegexLiteral(value) {
106
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
107
+ }
108
+ export function matchesRouteTemplate(pathname, template) {
109
+ const input = (pathname || "").toString();
110
+ const pattern = (template || "").toString();
111
+ if (!input.startsWith("/") || !pattern.startsWith("/"))
112
+ return false;
113
+ if (input === pattern)
114
+ return true;
115
+ // Convert "/dashboard/projects/:id/pages" or "/use-cases/[slug]" into a safe regex.
116
+ const parts = pattern.split("/").filter(Boolean);
117
+ const regexParts = parts.map((part) => {
118
+ if (part === "*")
119
+ return ".*";
120
+ if (part.startsWith(":"))
121
+ return "[^/]+";
122
+ if (part.startsWith("[") && part.endsWith("]"))
123
+ return "[^/]+";
124
+ return escapeRegexLiteral(part);
125
+ });
126
+ const regex = new RegExp(`^/${regexParts.join("/")}$`);
127
+ return regex.test(input);
128
+ }
129
+ export function isInactivePagePath(pathname, inactivePages) {
130
+ const input = (pathname || "").toString();
131
+ if (!input.startsWith("/"))
132
+ return false;
133
+ if (!Array.isArray(inactivePages) || inactivePages.length === 0)
134
+ return false;
135
+ return inactivePages.some((pattern) => matchesRouteTemplate(input, pattern));
136
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.5.3";
1
+ export declare const VERSION = "0.5.5";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.5.3";
1
+ export const VERSION = "0.5.5";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovalingo/lovalingo",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
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",
@@ -28,7 +28,7 @@
28
28
  "lovable",
29
29
  "vibe-coded"
30
30
  ],
31
- "author": "Mertens Advies",
31
+ "author": "Lovalingo Swiss",
32
32
  "license": "UNLICENSED",
33
33
  "private": false,
34
34
  "peerDependencies": {