@lovalingo/lovalingo 0.5.2 → 0.5.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.
- package/LICENSE +8 -8
- package/README.md +3 -3
- package/dist/components/AixsterProvider.js +67 -35
- package/dist/components/LangLink.js +9 -7
- package/dist/components/LangRouter.d.ts +4 -1
- package/dist/components/LangRouter.js +132 -28
- package/dist/components/LanguageSwitcher.js +6 -4
- package/dist/context/LangRoutingContext.d.ts +8 -0
- package/dist/context/LangRoutingContext.js +7 -0
- package/dist/hooks/useLangNavigate.js +12 -12
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/utils/api.d.ts +9 -0
- package/dist/utils/nonLocalizedPaths.d.ts +12 -0
- package/dist/utils/nonLocalizedPaths.js +136 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/LICENSE
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
LOVALINGO COMMERCIAL LICENSE
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2025
|
|
3
|
+
Copyright (c) 2025 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
142
|
+
please contact Lovalingo Swiss.
|
|
143
143
|
|
|
144
144
|
MERTENS ADVIES
|
|
145
145
|
Lovalingo Translation Platform
|
|
146
146
|
© 2025 All Rights Reserved
|
|
147
147
|
|
|
148
|
-
Website: [Contact
|
|
148
|
+
Website: [Contact Lovalingo Swiss for licensing information]
|
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ export function App() {
|
|
|
55
55
|
const navigateRef = useRef();
|
|
56
56
|
|
|
57
57
|
return (
|
|
58
|
-
<LangRouter defaultLang="en" langs={["en", "de", "fr"]} navigateRef={navigateRef}>
|
|
58
|
+
<LangRouter publicAnonKey="aix_your_public_anon_key" defaultLang="en" langs={["en", "de", "fr"]} navigateRef={navigateRef}>
|
|
59
59
|
<LovalingoProvider
|
|
60
60
|
publicAnonKey="aix_your_public_anon_key"
|
|
61
61
|
defaultLocale="en"
|
|
@@ -349,7 +349,7 @@ export async function GET() {
|
|
|
349
349
|
|
|
350
350
|
**COMMERCIAL LICENSE - NOT OPEN SOURCE**
|
|
351
351
|
|
|
352
|
-
Copyright (c) 2025
|
|
352
|
+
Copyright (c) 2025 Lovalingo Swiss. All rights reserved.
|
|
353
353
|
|
|
354
354
|
### For Agencies & Developers
|
|
355
355
|
|
|
@@ -374,6 +374,6 @@ applications containing Lovalingo, but may not modify, redistribute, or extract
|
|
|
374
374
|
|
|
375
375
|
This software is licensed under the **Lovalingo Commercial License**.
|
|
376
376
|
This is NOT open source software. All intellectual property rights remain the
|
|
377
|
-
exclusive property of
|
|
377
|
+
exclusive property of Lovalingo Swiss.
|
|
378
378
|
|
|
379
379
|
See LICENSE file for complete terms and conditions.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import React, { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
|
1
|
+
import React, { useMemo, useState, useEffect, useCallback, useRef, useContext } from 'react';
|
|
2
2
|
import { LovalingoContext } from '../context/LovalingoContext';
|
|
3
|
+
import { LangRoutingContext } from '../context/LangRoutingContext';
|
|
3
4
|
import { LovalingoAPI } from '../utils/api';
|
|
4
5
|
import { applyDomRules } from '../utils/domRules';
|
|
5
6
|
import { hashContent } from '../utils/hash';
|
|
6
7
|
import { applyActiveTranslations, getCriticalFingerprint, restoreDom, setActiveTranslations, setMarkerEngineExclusions, startMarkerEngine } from '../utils/markerEngine';
|
|
7
8
|
import { logDebug, warnDebug, errorDebug } from '../utils/logger';
|
|
9
|
+
import { isNonLocalizedPath, stripLocalePrefix } from '../utils/nonLocalizedPaths';
|
|
8
10
|
import { processPath } from '../utils/pathNormalizer';
|
|
9
11
|
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
10
12
|
const LOCALE_STORAGE_KEY = 'Lovalingo_locale';
|
|
@@ -75,6 +77,7 @@ navigateRef, // For path mode routing
|
|
|
75
77
|
useEffect(() => {
|
|
76
78
|
apiRef.current = new LovalingoAPI(resolvedApiKey, apiBase, enhancedPathConfig);
|
|
77
79
|
}, [apiBase, enhancedPathConfig, resolvedApiKey]);
|
|
80
|
+
const routingConfig = useContext(LangRoutingContext);
|
|
78
81
|
const [entitlements, setEntitlements] = useState(() => apiRef.current.getEntitlements());
|
|
79
82
|
const lastPageviewRef = useRef("");
|
|
80
83
|
const lastPageviewFingerprintRef = useRef("");
|
|
@@ -111,6 +114,7 @@ navigateRef, // For path mode routing
|
|
|
111
114
|
const prehideStateRef = useRef({
|
|
112
115
|
active: false,
|
|
113
116
|
timeoutId: null,
|
|
117
|
+
startedAtMs: null,
|
|
114
118
|
prevHtmlVisibility: "",
|
|
115
119
|
prevBodyVisibility: "",
|
|
116
120
|
prevHtmlBg: "",
|
|
@@ -195,6 +199,27 @@ navigateRef, // For path mode routing
|
|
|
195
199
|
pageviewFingerprintTimeoutRef.current = window.setTimeout(trySendFingerprint, 800);
|
|
196
200
|
pageviewFingerprintRetryTimeoutRef.current = window.setTimeout(trySendFingerprint, 2000);
|
|
197
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
|
+
}, []);
|
|
198
223
|
const enablePrehide = useCallback((bgColor) => {
|
|
199
224
|
if (typeof document === "undefined")
|
|
200
225
|
return;
|
|
@@ -203,8 +228,13 @@ navigateRef, // For path mode routing
|
|
|
203
228
|
if (!html || !body)
|
|
204
229
|
return;
|
|
205
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
|
+
}
|
|
206
235
|
if (!state.active) {
|
|
207
236
|
state.active = true;
|
|
237
|
+
state.startedAtMs = Date.now();
|
|
208
238
|
state.prevHtmlVisibility = html.style.visibility || "";
|
|
209
239
|
state.prevBodyVisibility = body.style.visibility || "";
|
|
210
240
|
state.prevHtmlBg = html.style.backgroundColor || "";
|
|
@@ -220,28 +250,9 @@ navigateRef, // For path mode routing
|
|
|
220
250
|
return;
|
|
221
251
|
}
|
|
222
252
|
// Why: avoid a "perma-hide" when navigation events repeatedly re-trigger prehide and keep extending the timeout.
|
|
223
|
-
state.timeoutId = window.setTimeout(() =>
|
|
224
|
-
}, []);
|
|
225
|
-
const disablePrehide =
|
|
226
|
-
if (typeof document === "undefined")
|
|
227
|
-
return;
|
|
228
|
-
const html = document.documentElement;
|
|
229
|
-
const body = document.body;
|
|
230
|
-
if (!html || !body)
|
|
231
|
-
return;
|
|
232
|
-
const state = prehideStateRef.current;
|
|
233
|
-
if (state.timeoutId != null) {
|
|
234
|
-
window.clearTimeout(state.timeoutId);
|
|
235
|
-
state.timeoutId = null;
|
|
236
|
-
}
|
|
237
|
-
if (!state.active)
|
|
238
|
-
return;
|
|
239
|
-
state.active = false;
|
|
240
|
-
html.style.visibility = state.prevHtmlVisibility;
|
|
241
|
-
body.style.visibility = state.prevBodyVisibility;
|
|
242
|
-
html.style.backgroundColor = state.prevHtmlBg;
|
|
243
|
-
body.style.backgroundColor = state.prevBodyBg;
|
|
244
|
-
}, []);
|
|
253
|
+
state.timeoutId = window.setTimeout(() => forceDisablePrehide(), PREHIDE_FAILSAFE_MS);
|
|
254
|
+
}, [forceDisablePrehide]);
|
|
255
|
+
const disablePrehide = forceDisablePrehide;
|
|
245
256
|
const buildCriticalCacheKey = useCallback((targetLocale, normalizedPath) => {
|
|
246
257
|
const key = `${resolvedApiKey || "anonymous"}:${targetLocale}:${normalizedPath || "/"}`;
|
|
247
258
|
return `${CRITICAL_CACHE_PREFIX}:${hashContent(key)}`;
|
|
@@ -554,6 +565,17 @@ navigateRef, // For path mode routing
|
|
|
554
565
|
isNavigatingRef.current = false;
|
|
555
566
|
return;
|
|
556
567
|
}
|
|
568
|
+
if (routing === "path") {
|
|
569
|
+
const stripped = stripLocalePrefix(window.location.pathname, allLocales);
|
|
570
|
+
if (isNonLocalizedPath(stripped, routingConfig.nonLocalizedPaths)) {
|
|
571
|
+
// Why: auth/admin (non-localized) routes must never be blocked or mutated by the translation runtime.
|
|
572
|
+
disablePrehide();
|
|
573
|
+
setActiveTranslations(null);
|
|
574
|
+
restoreDom(document.body);
|
|
575
|
+
isNavigatingRef.current = false;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
557
579
|
const currentPath = window.location.pathname + window.location.search;
|
|
558
580
|
const normalizedPath = processPath(window.location.pathname, enhancedPathConfig);
|
|
559
581
|
const cacheKey = `${targetLocale}:${normalizedPath}`;
|
|
@@ -740,6 +762,7 @@ navigateRef, // For path mode routing
|
|
|
740
762
|
}
|
|
741
763
|
}, [
|
|
742
764
|
applySeoBundle,
|
|
765
|
+
allLocales,
|
|
743
766
|
autoApplyRules,
|
|
744
767
|
defaultLocale,
|
|
745
768
|
disablePrehide,
|
|
@@ -749,6 +772,8 @@ navigateRef, // For path mode routing
|
|
|
749
772
|
isSeoActive,
|
|
750
773
|
mode,
|
|
751
774
|
readCriticalCache,
|
|
775
|
+
routing,
|
|
776
|
+
routingConfig.nonLocalizedPaths,
|
|
752
777
|
setCachedLoadingBgColor,
|
|
753
778
|
toTranslations,
|
|
754
779
|
writeCriticalCache,
|
|
@@ -837,6 +862,13 @@ navigateRef, // For path mode routing
|
|
|
837
862
|
isNavigatingRef.current = true;
|
|
838
863
|
// Update URL based on routing strategy
|
|
839
864
|
if (routing === 'path') {
|
|
865
|
+
const stripped = stripLocalePrefix(window.location.pathname, allLocales);
|
|
866
|
+
if (isNonLocalizedPath(stripped, routingConfig.nonLocalizedPaths)) {
|
|
867
|
+
// Why: switching languages must not rewrite non-localized routes like "/auth" to "/de/auth".
|
|
868
|
+
setLocaleState(newLocale);
|
|
869
|
+
isNavigatingRef.current = false;
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
840
872
|
const pathParts = window.location.pathname.split('/').filter(Boolean);
|
|
841
873
|
// Strip existing locale
|
|
842
874
|
if (allLocales.includes(pathParts[0])) {
|
|
@@ -869,7 +901,7 @@ navigateRef, // For path mode routing
|
|
|
869
901
|
})().finally(() => {
|
|
870
902
|
isInternalNavigationRef.current = false;
|
|
871
903
|
});
|
|
872
|
-
}, [allLocales, locale, routing, loadData, navigateRef]);
|
|
904
|
+
}, [allLocales, defaultLocale, locale, routing, loadData, navigateRef, routingConfig.nonLocalizedPaths]);
|
|
873
905
|
// No string-level "misses" from the client: the pipeline (Browser Rendering) is source-of-truth for string discovery.
|
|
874
906
|
// Initialize
|
|
875
907
|
useEffect(() => {
|
|
@@ -929,13 +961,6 @@ navigateRef, // For path mode routing
|
|
|
929
961
|
if (!autoPrefixLinks)
|
|
930
962
|
return;
|
|
931
963
|
const supportedLocales = allLocales;
|
|
932
|
-
const isAssetPath = (pathname) => {
|
|
933
|
-
if (pathname === '/robots.txt' || pathname === '/sitemap.xml')
|
|
934
|
-
return true;
|
|
935
|
-
if (pathname.startsWith('/.well-known/'))
|
|
936
|
-
return true;
|
|
937
|
-
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);
|
|
938
|
-
};
|
|
939
964
|
const shouldProcessCurrentPath = () => {
|
|
940
965
|
const parts = window.location.pathname.split('/').filter(Boolean);
|
|
941
966
|
return parts.length > 0 && supportedLocales.includes(parts[0]);
|
|
@@ -963,7 +988,7 @@ navigateRef, // For path mode routing
|
|
|
963
988
|
}
|
|
964
989
|
if (url.origin !== window.location.origin)
|
|
965
990
|
return null;
|
|
966
|
-
if (
|
|
991
|
+
if (isNonLocalizedPath(url.pathname, routingConfig.nonLocalizedPaths))
|
|
967
992
|
return null;
|
|
968
993
|
const parts = url.pathname.split('/').filter(Boolean);
|
|
969
994
|
// Root ("/") should be locale-prefixed too (e.g. clicking a logo linking to "https://example.com")
|
|
@@ -1070,7 +1095,7 @@ navigateRef, // For path mode routing
|
|
|
1070
1095
|
mo.disconnect();
|
|
1071
1096
|
document.removeEventListener('click', onClickCapture, true);
|
|
1072
1097
|
};
|
|
1073
|
-
}, [routing, autoPrefixLinks, allLocales, locale, navigateRef]);
|
|
1098
|
+
}, [routing, autoPrefixLinks, allLocales, locale, navigateRef, routingConfig.nonLocalizedPaths]);
|
|
1074
1099
|
// Navigation prefetch: warm the HTTP cache for bootstrap + page bundle before the user clicks (reduces EN→FR flash on SPA route changes).
|
|
1075
1100
|
useEffect(() => {
|
|
1076
1101
|
if (!resolvedApiKey)
|
|
@@ -1182,9 +1207,16 @@ navigateRef, // For path mode routing
|
|
|
1182
1207
|
};
|
|
1183
1208
|
return (React.createElement(LovalingoContext.Provider, { value: contextValue },
|
|
1184
1209
|
children,
|
|
1185
|
-
|
|
1210
|
+
(() => {
|
|
1211
|
+
if (routing !== "path")
|
|
1212
|
+
return true;
|
|
1213
|
+
if (typeof window === "undefined")
|
|
1214
|
+
return true;
|
|
1215
|
+
const stripped = stripLocalePrefix(window.location.pathname, allLocales);
|
|
1216
|
+
return !isNonLocalizedPath(stripped, routingConfig.nonLocalizedPaths);
|
|
1217
|
+
})() && (React.createElement(LanguageSwitcher, { locales: allLocales, currentLocale: locale, onLocaleChange: setLocale, position: switcherPosition, offsetY: switcherOffsetY, theme: switcherTheme, branding: {
|
|
1186
1218
|
required: Boolean(entitlements?.brandingRequired),
|
|
1187
1219
|
enabled: brandingEnabled,
|
|
1188
1220
|
href: "https://lovalingo.com",
|
|
1189
|
-
} })));
|
|
1221
|
+
} }))));
|
|
1190
1222
|
};
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Link } from 'react-router-dom';
|
|
3
3
|
import { useLang } from '../hooks/useLang';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
]);
|
|
4
|
+
import { useContext } from 'react';
|
|
5
|
+
import { LangRoutingContext } from '../context/LangRoutingContext';
|
|
6
|
+
import { isNonLocalizedPath } from '../utils/nonLocalizedPaths';
|
|
8
7
|
/**
|
|
9
8
|
* LangLink - Language-aware Link component
|
|
10
9
|
*
|
|
@@ -24,12 +23,15 @@ const NON_LOCALIZED_APP_PATHS = new Set([
|
|
|
24
23
|
*/
|
|
25
24
|
export function LangLink({ to, ...props }) {
|
|
26
25
|
const lang = useLang();
|
|
26
|
+
const routing = useContext(LangRoutingContext);
|
|
27
27
|
// If 'to' is a string, prepend language
|
|
28
28
|
const langTo = typeof to === 'string'
|
|
29
29
|
? (() => {
|
|
30
|
-
const trimmed = to
|
|
31
|
-
const
|
|
32
|
-
return
|
|
30
|
+
const trimmed = (to || '').toString().trim();
|
|
31
|
+
const normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
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 });
|
|
@@ -4,6 +4,9 @@ interface LangRouterProps {
|
|
|
4
4
|
defaultLang: string;
|
|
5
5
|
langs: string[];
|
|
6
6
|
navigateRef?: React.MutableRefObject<((path: string) => void) | undefined>;
|
|
7
|
+
apiBase?: string;
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
publicAnonKey?: string;
|
|
7
10
|
}
|
|
8
11
|
/**
|
|
9
12
|
* LangRouter - Drop-in replacement for BrowserRouter that automatically handles language routing
|
|
@@ -30,5 +33,5 @@ interface LangRouterProps {
|
|
|
30
33
|
* - /fr/pricing
|
|
31
34
|
* - etc.
|
|
32
35
|
*/
|
|
33
|
-
export declare function LangRouter({ children, defaultLang, langs, navigateRef }: LangRouterProps): React.JSX.Element;
|
|
36
|
+
export declare function LangRouter({ children, defaultLang, langs, navigateRef, apiBase, apiKey, publicAnonKey }: LangRouterProps): React.JSX.Element;
|
|
34
37
|
export {};
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import React, { useEffect } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { BrowserRouter, Routes, Route, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|
3
3
|
import { LangContext } from '../context/LangContext';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
]);
|
|
8
|
-
function isNonLocalizedPath(pathname) {
|
|
9
|
-
if (NON_LOCALIZED_APP_PATHS.has(pathname))
|
|
10
|
-
return true;
|
|
11
|
-
if (pathname.startsWith('/.well-known/'))
|
|
12
|
-
return true;
|
|
13
|
-
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);
|
|
14
|
-
}
|
|
4
|
+
import { LangRoutingContext } from '../context/LangRoutingContext';
|
|
5
|
+
import { isNonLocalizedPath, parseBootstrapInactivePages, parseBootstrapNonLocalizedPaths } from '../utils/nonLocalizedPaths';
|
|
6
|
+
import { logDebug } from '../utils/logger';
|
|
15
7
|
/**
|
|
16
8
|
* NavigateExporter - Internal component that exports navigate function via ref
|
|
17
9
|
*/
|
|
@@ -27,13 +19,14 @@ function NavigateExporter({ navigateRef }) {
|
|
|
27
19
|
/**
|
|
28
20
|
* LangGuard - Internal component that validates language and provides it to children
|
|
29
21
|
*/
|
|
30
|
-
function LangGuard({ defaultLang,
|
|
22
|
+
function LangGuard({ lang, nonLocalizedPaths, defaultLang, }) {
|
|
31
23
|
const location = useLocation();
|
|
32
24
|
// If the URL is language-prefixed but the underlying route is non-localized (e.g. robots/sitemap),
|
|
33
25
|
// redirect to the canonical non-localized path.
|
|
34
26
|
const prefix = `/${lang}`;
|
|
35
27
|
const restPath = location.pathname.startsWith(prefix) ? location.pathname.slice(prefix.length) || '/' : location.pathname;
|
|
36
|
-
|
|
28
|
+
// Why: only explicit non-localized rules may strip the locale prefix; inactive pages must never affect client routing.
|
|
29
|
+
if (isNonLocalizedPath(restPath, nonLocalizedPaths)) {
|
|
37
30
|
const nextPath = `${restPath}${location.search}${location.hash}`;
|
|
38
31
|
return React.createElement(Navigate, { to: nextPath, replace: true });
|
|
39
32
|
}
|
|
@@ -41,15 +34,24 @@ function LangGuard({ defaultLang, lang }) {
|
|
|
41
34
|
return (React.createElement(LangContext.Provider, { value: lang },
|
|
42
35
|
React.createElement(Outlet, { context: { lang } })));
|
|
43
36
|
}
|
|
44
|
-
function RedirectToDefaultLang({ defaultLang, children }) {
|
|
37
|
+
function RedirectToDefaultLang({ defaultLang, children, nonLocalizedPaths, routingStatus, }) {
|
|
45
38
|
const location = useLocation();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
const navigate = useNavigate();
|
|
40
|
+
const shouldSkip = isNonLocalizedPath(location.pathname, nonLocalizedPaths);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (shouldSkip)
|
|
43
|
+
return;
|
|
44
|
+
if (routingStatus === "loading")
|
|
45
|
+
return;
|
|
46
|
+
const nextPath = location.pathname === "/" || location.pathname === ""
|
|
47
|
+
? `/${defaultLang}${location.search}${location.hash}`
|
|
48
|
+
: `/${defaultLang}${location.pathname}${location.search}${location.hash}`;
|
|
49
|
+
const current = `${location.pathname}${location.search}${location.hash}`;
|
|
50
|
+
if (nextPath === current)
|
|
51
|
+
return;
|
|
52
|
+
navigate(nextPath, { replace: true });
|
|
53
|
+
}, [defaultLang, location.hash, location.pathname, location.search, navigate, routingStatus, shouldSkip]);
|
|
54
|
+
return React.createElement(React.Fragment, null, children);
|
|
53
55
|
}
|
|
54
56
|
/**
|
|
55
57
|
* LangRouter - Drop-in replacement for BrowserRouter that automatically handles language routing
|
|
@@ -76,12 +78,114 @@ function RedirectToDefaultLang({ defaultLang, children }) {
|
|
|
76
78
|
* - /fr/pricing
|
|
77
79
|
* - etc.
|
|
78
80
|
*/
|
|
79
|
-
export function LangRouter({ children, defaultLang, langs, navigateRef }) {
|
|
81
|
+
export function LangRouter({ children, defaultLang, langs, navigateRef, apiBase, apiKey, publicAnonKey }) {
|
|
82
|
+
const metaKey = typeof document !== "undefined"
|
|
83
|
+
? document.querySelector('meta[name="lovalingo-public-anon-key"]')?.content?.trim() || ""
|
|
84
|
+
: "";
|
|
85
|
+
const globals = globalThis;
|
|
86
|
+
const resolvedApiKey = (typeof apiKey === "string" && apiKey.trim().length > 0
|
|
87
|
+
? apiKey
|
|
88
|
+
: typeof publicAnonKey === "string" && publicAnonKey.trim().length > 0
|
|
89
|
+
? publicAnonKey
|
|
90
|
+
: globals.__LOVALINGO_PUBLIC_ANON_KEY__ || globals.__LOVALINGO_API_KEY__ || metaKey || "").trim();
|
|
91
|
+
const resolvedApiBase = (typeof apiBase === "string" && apiBase.trim().length > 0
|
|
92
|
+
? apiBase.trim()
|
|
93
|
+
: "https://cdn.lovalingo.com");
|
|
94
|
+
const nonLocalizedStorageKey = useMemo(() => `Lovalingo_non_localized_paths:${resolvedApiKey || "anonymous"}`, [resolvedApiKey]);
|
|
95
|
+
const inactivePagesStorageKey = useMemo(() => `Lovalingo_inactive_pages:${resolvedApiKey || "anonymous"}`, [resolvedApiKey]);
|
|
96
|
+
const [nonLocalizedPaths, setNonLocalizedPaths] = useState(() => {
|
|
97
|
+
if (typeof window === "undefined")
|
|
98
|
+
return [];
|
|
99
|
+
if (!resolvedApiKey)
|
|
100
|
+
return [];
|
|
101
|
+
try {
|
|
102
|
+
const raw = localStorage.getItem(nonLocalizedStorageKey);
|
|
103
|
+
if (!raw)
|
|
104
|
+
return [];
|
|
105
|
+
const parsed = JSON.parse(raw);
|
|
106
|
+
return parseBootstrapNonLocalizedPaths(parsed);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
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
|
+
});
|
|
128
|
+
const [routingStatus, setRoutingStatus] = useState(() => {
|
|
129
|
+
if (!resolvedApiKey)
|
|
130
|
+
return "unknown";
|
|
131
|
+
return nonLocalizedPaths.length > 0 || inactivePages.length > 0 ? "ready" : "loading";
|
|
132
|
+
});
|
|
133
|
+
const fetchRoutingConfig = useCallback(async () => {
|
|
134
|
+
if (typeof window === "undefined")
|
|
135
|
+
return;
|
|
136
|
+
if (!resolvedApiKey)
|
|
137
|
+
return;
|
|
138
|
+
const pathParam = window.location.pathname + window.location.search;
|
|
139
|
+
const requestUrl = `${resolvedApiBase}/functions/v1/bootstrap?key=${encodeURIComponent(resolvedApiKey)}&locale=${encodeURIComponent(defaultLang)}&path=${encodeURIComponent(pathParam)}`;
|
|
140
|
+
const response = await fetch(requestUrl);
|
|
141
|
+
const resolvedResponse = response.status === 304 ? await fetch(requestUrl, { cache: "force-cache" }) : response;
|
|
142
|
+
if (!resolvedResponse.ok)
|
|
143
|
+
throw new Error(`bootstrap HTTP ${resolvedResponse.status}`);
|
|
144
|
+
const data = (await resolvedResponse.json());
|
|
145
|
+
const record = (data || {});
|
|
146
|
+
return {
|
|
147
|
+
nonLocalizedPaths: parseBootstrapNonLocalizedPaths(record["non_localized_paths"]),
|
|
148
|
+
inactivePages: parseBootstrapInactivePages(record["inactive_pages"]),
|
|
149
|
+
};
|
|
150
|
+
}, [defaultLang, resolvedApiBase, resolvedApiKey]);
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
let cancelled = false;
|
|
153
|
+
void (async () => {
|
|
154
|
+
if (!resolvedApiKey)
|
|
155
|
+
return;
|
|
156
|
+
setRoutingStatus((prev) => (prev === "ready" ? prev : "loading"));
|
|
157
|
+
try {
|
|
158
|
+
const next = await fetchRoutingConfig();
|
|
159
|
+
if (cancelled || !next)
|
|
160
|
+
return;
|
|
161
|
+
setNonLocalizedPaths(next.nonLocalizedPaths);
|
|
162
|
+
setInactivePages(next.inactivePages);
|
|
163
|
+
setRoutingStatus("ready");
|
|
164
|
+
try {
|
|
165
|
+
localStorage.setItem(nonLocalizedStorageKey, JSON.stringify(next.nonLocalizedPaths));
|
|
166
|
+
localStorage.setItem(inactivePagesStorageKey, JSON.stringify(next.inactivePages));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// ignore
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
if (cancelled)
|
|
174
|
+
return;
|
|
175
|
+
setRoutingStatus("error");
|
|
176
|
+
logDebug("[Lovalingo] Failed to fetch routing config:", err);
|
|
177
|
+
}
|
|
178
|
+
})();
|
|
179
|
+
return () => {
|
|
180
|
+
cancelled = true;
|
|
181
|
+
};
|
|
182
|
+
}, [fetchRoutingConfig, inactivePagesStorageKey, nonLocalizedStorageKey, resolvedApiKey]);
|
|
80
183
|
return (React.createElement(BrowserRouter, null,
|
|
81
184
|
React.createElement(NavigateExporter, { navigateRef: navigateRef }),
|
|
82
|
-
React.createElement(
|
|
83
|
-
|
|
84
|
-
React.createElement(Route, {
|
|
85
|
-
|
|
86
|
-
|
|
185
|
+
React.createElement(LangRoutingContext.Provider, { value: { defaultLang, nonLocalizedPaths, inactivePages, status: routingStatus } },
|
|
186
|
+
React.createElement(Routes, null,
|
|
187
|
+
langs.map((lang) => (React.createElement(Route, { key: lang, path: `${lang}/*`, element: React.createElement(LangGuard, { lang: lang, nonLocalizedPaths: nonLocalizedPaths, defaultLang: defaultLang }) },
|
|
188
|
+
React.createElement(Route, { index: true, element: React.createElement(React.Fragment, null, children) }),
|
|
189
|
+
React.createElement(Route, { path: "*", element: React.createElement(React.Fragment, null, children) })))),
|
|
190
|
+
React.createElement(Route, { path: "*", element: React.createElement(RedirectToDefaultLang, { defaultLang: defaultLang, nonLocalizedPaths: nonLocalizedPaths, routingStatus: routingStatus }, children) })))));
|
|
87
191
|
}
|
|
@@ -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: '
|
|
214
|
-
|
|
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: "
|
|
222
|
-
React.createElement("path", { d: "
|
|
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
|
" ",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NonLocalizedPathRule } from "../utils/nonLocalizedPaths";
|
|
2
|
+
export type LangRoutingContextValue = {
|
|
3
|
+
defaultLang: string;
|
|
4
|
+
nonLocalizedPaths: NonLocalizedPathRule[];
|
|
5
|
+
inactivePages: string[];
|
|
6
|
+
status: "unknown" | "loading" | "ready" | "error";
|
|
7
|
+
};
|
|
8
|
+
export declare const LangRoutingContext: import("react").Context<LangRoutingContextValue>;
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { useNavigate } from 'react-router-dom';
|
|
2
|
-
import { useCallback } from 'react';
|
|
2
|
+
import { useCallback, useContext } from 'react';
|
|
3
3
|
import { useLang } from './useLang';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
'robots.txt',
|
|
7
|
-
'sitemap.xml',
|
|
8
|
-
]);
|
|
4
|
+
import { LangRoutingContext } from '../context/LangRoutingContext';
|
|
5
|
+
import { isNonLocalizedPath } from '../utils/nonLocalizedPaths';
|
|
9
6
|
/**
|
|
10
7
|
* useLangNavigate - Get a language-aware navigate function
|
|
11
8
|
*
|
|
@@ -29,12 +26,15 @@ const NON_LOCALIZED_APP_PATHS = new Set([
|
|
|
29
26
|
export function useLangNavigate() {
|
|
30
27
|
const navigate = useNavigate();
|
|
31
28
|
const lang = useLang();
|
|
29
|
+
const routing = useContext(LangRoutingContext);
|
|
32
30
|
return useCallback((path, options) => {
|
|
33
|
-
const trimmed = path.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
const trimmed = (path || "").toString().trim();
|
|
32
|
+
if (!trimmed)
|
|
33
|
+
return;
|
|
34
|
+
const normalized = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
35
|
+
const fullPath = isNonLocalizedPath(normalized, routing.nonLocalizedPaths)
|
|
36
|
+
? normalized
|
|
37
|
+
: `/${lang}${normalized}`;
|
|
38
38
|
navigate(fullPath, options);
|
|
39
|
-
}, [lang, navigate]);
|
|
39
|
+
}, [lang, navigate, routing.nonLocalizedPaths]);
|
|
40
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
|
|
4
|
+
* Copyright (c) 2025 Lovalingo Swiss. All rights reserved.
|
|
5
5
|
*
|
|
6
|
-
* This software is the intellectual property of
|
|
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
|
|
4
|
+
* Copyright (c) 2025 Lovalingo Swiss. All rights reserved.
|
|
5
5
|
*
|
|
6
|
-
* This software is the intellectual property of
|
|
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/utils/api.d.ts
CHANGED
|
@@ -25,6 +25,15 @@ export type BootstrapResponse = {
|
|
|
25
25
|
locale?: string;
|
|
26
26
|
normalized_path?: string;
|
|
27
27
|
routing_strategy?: string;
|
|
28
|
+
non_localized_paths?: Array<{
|
|
29
|
+
pattern?: string;
|
|
30
|
+
match_type?: string;
|
|
31
|
+
updated_at?: string | null;
|
|
32
|
+
}>;
|
|
33
|
+
inactive_pages?: Array<{
|
|
34
|
+
page_path?: string;
|
|
35
|
+
updated_at?: string | null;
|
|
36
|
+
}>;
|
|
28
37
|
loading_bg_color?: string | null;
|
|
29
38
|
branding_enabled?: boolean;
|
|
30
39
|
seoEnabled?: boolean;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type NonLocalizedPathRule = {
|
|
2
|
+
pattern: string;
|
|
3
|
+
match_type: "exact" | "prefix" | "regex";
|
|
4
|
+
};
|
|
5
|
+
export declare function isGlobalNonLocalizedPath(pathname: string): boolean;
|
|
6
|
+
export declare function matchesNonLocalizedRules(pathname: string, rules: NonLocalizedPathRule[]): boolean;
|
|
7
|
+
export declare function isNonLocalizedPath(pathname: string, rules: NonLocalizedPathRule[]): boolean;
|
|
8
|
+
export declare function stripLocalePrefix(pathname: string, locales: string[]): string;
|
|
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;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const GLOBAL_NON_LOCALIZED_APP_PATHS = new Set(["/robots.txt", "/sitemap.xml"]);
|
|
2
|
+
export function isGlobalNonLocalizedPath(pathname) {
|
|
3
|
+
const input = (pathname || "").toString();
|
|
4
|
+
if (!input.startsWith("/"))
|
|
5
|
+
return false;
|
|
6
|
+
if (GLOBAL_NON_LOCALIZED_APP_PATHS.has(input))
|
|
7
|
+
return true;
|
|
8
|
+
if (input.startsWith("/.well-known/"))
|
|
9
|
+
return true;
|
|
10
|
+
return /\.(?:png|jpg|jpeg|gif|svg|webp|avif|ico|css|js|map|json|xml|txt|pdf|zip|gz|br|woff2?|ttf|eot)$/i.test(input);
|
|
11
|
+
}
|
|
12
|
+
export function matchesNonLocalizedRules(pathname, rules) {
|
|
13
|
+
const input = (pathname || "").toString();
|
|
14
|
+
if (!input.startsWith("/"))
|
|
15
|
+
return false;
|
|
16
|
+
if (!Array.isArray(rules) || rules.length === 0)
|
|
17
|
+
return false;
|
|
18
|
+
for (const rule of rules) {
|
|
19
|
+
const pattern = typeof rule?.pattern === "string" ? rule.pattern : "";
|
|
20
|
+
const matchType = rule?.match_type;
|
|
21
|
+
if (!pattern)
|
|
22
|
+
continue;
|
|
23
|
+
if (matchType === "exact") {
|
|
24
|
+
if (input === pattern)
|
|
25
|
+
return true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (matchType === "prefix") {
|
|
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}/`))
|
|
35
|
+
return true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (matchType === "regex") {
|
|
39
|
+
try {
|
|
40
|
+
if (new RegExp(pattern).test(input))
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// ignore invalid regex rules
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
export function isNonLocalizedPath(pathname, rules) {
|
|
51
|
+
return isGlobalNonLocalizedPath(pathname) || matchesNonLocalizedRules(pathname, rules);
|
|
52
|
+
}
|
|
53
|
+
export function stripLocalePrefix(pathname, locales) {
|
|
54
|
+
const input = (pathname || "").toString();
|
|
55
|
+
if (!input.startsWith("/"))
|
|
56
|
+
return input;
|
|
57
|
+
const parts = input.split("/").filter(Boolean);
|
|
58
|
+
if (parts.length === 0)
|
|
59
|
+
return "/";
|
|
60
|
+
const first = parts[0] || "";
|
|
61
|
+
if (!first || !Array.isArray(locales) || !locales.includes(first))
|
|
62
|
+
return input;
|
|
63
|
+
const rest = `/${parts.slice(1).join("/")}`;
|
|
64
|
+
return rest === "" ? "/" : rest;
|
|
65
|
+
}
|
|
66
|
+
export function parseBootstrapNonLocalizedPaths(value) {
|
|
67
|
+
if (!Array.isArray(value))
|
|
68
|
+
return [];
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const row of value) {
|
|
71
|
+
if (!row || typeof row !== "object")
|
|
72
|
+
continue;
|
|
73
|
+
const record = row;
|
|
74
|
+
const pattern = typeof record.pattern === "string" ? record.pattern.trim() : "";
|
|
75
|
+
const match_type = typeof record.match_type === "string" ? record.match_type.trim() : "";
|
|
76
|
+
if (!pattern)
|
|
77
|
+
continue;
|
|
78
|
+
if (match_type !== "exact" && match_type !== "prefix" && match_type !== "regex")
|
|
79
|
+
continue;
|
|
80
|
+
out.push({ pattern, match_type: match_type });
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
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.
|
|
1
|
+
export declare const VERSION = "0.5.4";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.5.4";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovalingo/lovalingo",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
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": "
|
|
31
|
+
"author": "Lovalingo Swiss",
|
|
32
32
|
"license": "UNLICENSED",
|
|
33
33
|
"private": false,
|
|
34
34
|
"peerDependencies": {
|