@lovalingo/lovalingo 0.5.2 → 0.5.3

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
@@ -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"
@@ -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("");
@@ -554,6 +557,17 @@ navigateRef, // For path mode routing
554
557
  isNavigatingRef.current = false;
555
558
  return;
556
559
  }
560
+ if (routing === "path") {
561
+ const stripped = stripLocalePrefix(window.location.pathname, allLocales);
562
+ if (isNonLocalizedPath(stripped, routingConfig.nonLocalizedPaths)) {
563
+ // Why: auth/admin (non-localized) routes must never be blocked or mutated by the translation runtime.
564
+ disablePrehide();
565
+ setActiveTranslations(null);
566
+ restoreDom(document.body);
567
+ isNavigatingRef.current = false;
568
+ return;
569
+ }
570
+ }
557
571
  const currentPath = window.location.pathname + window.location.search;
558
572
  const normalizedPath = processPath(window.location.pathname, enhancedPathConfig);
559
573
  const cacheKey = `${targetLocale}:${normalizedPath}`;
@@ -837,6 +851,13 @@ navigateRef, // For path mode routing
837
851
  isNavigatingRef.current = true;
838
852
  // Update URL based on routing strategy
839
853
  if (routing === 'path') {
854
+ const stripped = stripLocalePrefix(window.location.pathname, allLocales);
855
+ if (isNonLocalizedPath(stripped, routingConfig.nonLocalizedPaths)) {
856
+ // Why: switching languages must not rewrite non-localized routes like "/auth" to "/de/auth".
857
+ setLocaleState(newLocale);
858
+ isNavigatingRef.current = false;
859
+ return;
860
+ }
840
861
  const pathParts = window.location.pathname.split('/').filter(Boolean);
841
862
  // Strip existing locale
842
863
  if (allLocales.includes(pathParts[0])) {
@@ -869,7 +890,7 @@ navigateRef, // For path mode routing
869
890
  })().finally(() => {
870
891
  isInternalNavigationRef.current = false;
871
892
  });
872
- }, [allLocales, locale, routing, loadData, navigateRef]);
893
+ }, [allLocales, locale, routing, loadData, navigateRef, routingConfig.nonLocalizedPaths]);
873
894
  // No string-level "misses" from the client: the pipeline (Browser Rendering) is source-of-truth for string discovery.
874
895
  // Initialize
875
896
  useEffect(() => {
@@ -929,13 +950,6 @@ navigateRef, // For path mode routing
929
950
  if (!autoPrefixLinks)
930
951
  return;
931
952
  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
953
  const shouldProcessCurrentPath = () => {
940
954
  const parts = window.location.pathname.split('/').filter(Boolean);
941
955
  return parts.length > 0 && supportedLocales.includes(parts[0]);
@@ -963,7 +977,7 @@ navigateRef, // For path mode routing
963
977
  }
964
978
  if (url.origin !== window.location.origin)
965
979
  return null;
966
- if (isAssetPath(url.pathname))
980
+ if (isNonLocalizedPath(url.pathname, routingConfig.nonLocalizedPaths))
967
981
  return null;
968
982
  const parts = url.pathname.split('/').filter(Boolean);
969
983
  // Root ("/") should be locale-prefixed too (e.g. clicking a logo linking to "https://example.com")
@@ -1070,7 +1084,7 @@ navigateRef, // For path mode routing
1070
1084
  mo.disconnect();
1071
1085
  document.removeEventListener('click', onClickCapture, true);
1072
1086
  };
1073
- }, [routing, autoPrefixLinks, allLocales, locale, navigateRef]);
1087
+ }, [routing, autoPrefixLinks, allLocales, locale, navigateRef, routingConfig.nonLocalizedPaths]);
1074
1088
  // Navigation prefetch: warm the HTTP cache for bootstrap + page bundle before the user clicks (reduces EN→FR flash on SPA route changes).
1075
1089
  useEffect(() => {
1076
1090
  if (!resolvedApiKey)
@@ -1182,9 +1196,16 @@ navigateRef, // For path mode routing
1182
1196
  };
1183
1197
  return (React.createElement(LovalingoContext.Provider, { value: contextValue },
1184
1198
  children,
1185
- React.createElement(LanguageSwitcher, { locales: allLocales, currentLocale: locale, onLocaleChange: setLocale, position: switcherPosition, offsetY: switcherOffsetY, theme: switcherTheme, branding: {
1199
+ (() => {
1200
+ if (routing !== "path")
1201
+ return true;
1202
+ if (typeof window === "undefined")
1203
+ return true;
1204
+ const stripped = stripLocalePrefix(window.location.pathname, allLocales);
1205
+ return !isNonLocalizedPath(stripped, routingConfig.nonLocalizedPaths);
1206
+ })() && (React.createElement(LanguageSwitcher, { locales: allLocales, currentLocale: locale, onLocaleChange: setLocale, position: switcherPosition, offsetY: switcherOffsetY, theme: switcherTheme, branding: {
1186
1207
  required: Boolean(entitlements?.brandingRequired),
1187
1208
  enabled: brandingEnabled,
1188
1209
  href: "https://lovalingo.com",
1189
- } })));
1210
+ } }))));
1190
1211
  };
@@ -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
- const NON_LOCALIZED_APP_PATHS = new Set([
5
- 'robots.txt',
6
- 'sitemap.xml',
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,13 @@ 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.replace(/^\//, '');
31
- const firstSegment = trimmed.split('/')[0] || '';
32
- return NON_LOCALIZED_APP_PATHS.has(firstSegment) ? `/${trimmed}` : `/${lang}/${trimmed}`;
30
+ const trimmed = (to || '').toString().trim();
31
+ const normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
32
+ return isNonLocalizedPath(normalized, routing.nonLocalizedPaths) ? normalized : `/${lang}${normalized}`;
33
33
  })()
34
34
  : to;
35
35
  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';
2
- import { BrowserRouter, Routes, Route, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { BrowserRouter, Routes, Route, Outlet, useLocation, useNavigate } from 'react-router-dom';
3
3
  import { LangContext } from '../context/LangContext';
4
- const NON_LOCALIZED_APP_PATHS = new Set([
5
- '/robots.txt',
6
- '/sitemap.xml',
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, 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,29 +19,42 @@ function NavigateExporter({ navigateRef }) {
27
19
  /**
28
20
  * LangGuard - Internal component that validates language and provides it to children
29
21
  */
30
- function LangGuard({ defaultLang, lang }) {
22
+ function LangGuard({ lang, nonLocalizedPaths, }) {
31
23
  const location = useLocation();
24
+ const navigate = useNavigate();
32
25
  // If the URL is language-prefixed but the underlying route is non-localized (e.g. robots/sitemap),
33
26
  // redirect to the canonical non-localized path.
34
27
  const prefix = `/${lang}`;
35
28
  const restPath = location.pathname.startsWith(prefix) ? location.pathname.slice(prefix.length) || '/' : location.pathname;
36
- if (isNonLocalizedPath(restPath)) {
29
+ const shouldDePrefix = isNonLocalizedPath(restPath, nonLocalizedPaths);
30
+ useEffect(() => {
31
+ if (!shouldDePrefix)
32
+ return;
37
33
  const nextPath = `${restPath}${location.search}${location.hash}`;
38
- return React.createElement(Navigate, { to: nextPath, replace: true });
39
- }
34
+ navigate(nextPath, { replace: true });
35
+ }, [location.hash, location.search, navigate, restPath, shouldDePrefix]);
40
36
  // Valid language - render children (user's routes)
41
37
  return (React.createElement(LangContext.Provider, { value: lang },
42
38
  React.createElement(Outlet, { context: { lang } })));
43
39
  }
44
- function RedirectToDefaultLang({ defaultLang, children }) {
40
+ function RedirectToDefaultLang({ defaultLang, children, nonLocalizedPaths, routingStatus, }) {
45
41
  const location = useLocation();
46
- if (isNonLocalizedPath(location.pathname)) {
47
- return React.createElement(React.Fragment, null, children);
48
- }
49
- const nextPath = location.pathname === '/' || location.pathname === ''
50
- ? `/${defaultLang}${location.search}${location.hash}`
51
- : `/${defaultLang}${location.pathname}${location.search}${location.hash}`;
52
- return React.createElement(Navigate, { to: nextPath, replace: true });
42
+ const navigate = useNavigate();
43
+ const shouldSkip = isNonLocalizedPath(location.pathname, nonLocalizedPaths);
44
+ useEffect(() => {
45
+ if (shouldSkip)
46
+ return;
47
+ if (routingStatus === "loading")
48
+ return;
49
+ const nextPath = location.pathname === "/" || location.pathname === ""
50
+ ? `/${defaultLang}${location.search}${location.hash}`
51
+ : `/${defaultLang}${location.pathname}${location.search}${location.hash}`;
52
+ const current = `${location.pathname}${location.search}${location.hash}`;
53
+ if (nextPath === current)
54
+ return;
55
+ navigate(nextPath, { replace: true });
56
+ }, [defaultLang, location.hash, location.pathname, location.search, navigate, routingStatus, shouldSkip]);
57
+ return React.createElement(React.Fragment, null, children);
53
58
  }
54
59
  /**
55
60
  * LangRouter - Drop-in replacement for BrowserRouter that automatically handles language routing
@@ -76,12 +81,92 @@ function RedirectToDefaultLang({ defaultLang, children }) {
76
81
  * - /fr/pricing
77
82
  * - etc.
78
83
  */
79
- export function LangRouter({ children, defaultLang, langs, navigateRef }) {
84
+ export function LangRouter({ children, defaultLang, langs, navigateRef, apiBase, apiKey, publicAnonKey }) {
85
+ const metaKey = typeof document !== "undefined"
86
+ ? document.querySelector('meta[name="lovalingo-public-anon-key"]')?.content?.trim() || ""
87
+ : "";
88
+ const globals = globalThis;
89
+ const resolvedApiKey = (typeof apiKey === "string" && apiKey.trim().length > 0
90
+ ? apiKey
91
+ : typeof publicAnonKey === "string" && publicAnonKey.trim().length > 0
92
+ ? publicAnonKey
93
+ : globals.__LOVALINGO_PUBLIC_ANON_KEY__ || globals.__LOVALINGO_API_KEY__ || metaKey || "").trim();
94
+ const resolvedApiBase = (typeof apiBase === "string" && apiBase.trim().length > 0
95
+ ? apiBase.trim()
96
+ : "https://cdn.lovalingo.com");
97
+ const nonLocalizedStorageKey = useMemo(() => `Lovalingo_non_localized_paths:${resolvedApiKey || "anonymous"}`, [resolvedApiKey]);
98
+ const [nonLocalizedPaths, setNonLocalizedPaths] = useState(() => {
99
+ if (typeof window === "undefined")
100
+ return [];
101
+ if (!resolvedApiKey)
102
+ return [];
103
+ try {
104
+ const raw = localStorage.getItem(nonLocalizedStorageKey);
105
+ if (!raw)
106
+ return [];
107
+ const parsed = JSON.parse(raw);
108
+ return parseBootstrapNonLocalizedPaths(parsed);
109
+ }
110
+ catch {
111
+ return [];
112
+ }
113
+ });
114
+ const [routingStatus, setRoutingStatus] = useState(() => {
115
+ if (!resolvedApiKey)
116
+ return "unknown";
117
+ return nonLocalizedPaths.length > 0 ? "ready" : "loading";
118
+ });
119
+ const fetchNonLocalizedPaths = useCallback(async () => {
120
+ if (typeof window === "undefined")
121
+ return;
122
+ if (!resolvedApiKey)
123
+ return;
124
+ const pathParam = window.location.pathname + window.location.search;
125
+ const requestUrl = `${resolvedApiBase}/functions/v1/bootstrap?key=${encodeURIComponent(resolvedApiKey)}&locale=${encodeURIComponent(defaultLang)}&path=${encodeURIComponent(pathParam)}`;
126
+ const response = await fetch(requestUrl);
127
+ const resolvedResponse = response.status === 304 ? await fetch(requestUrl, { cache: "force-cache" }) : response;
128
+ if (!resolvedResponse.ok)
129
+ throw new Error(`bootstrap HTTP ${resolvedResponse.status}`);
130
+ const data = (await resolvedResponse.json());
131
+ const record = (data || {});
132
+ return parseBootstrapNonLocalizedPaths(record["non_localized_paths"]);
133
+ }, [defaultLang, resolvedApiBase, resolvedApiKey]);
134
+ useEffect(() => {
135
+ let cancelled = false;
136
+ void (async () => {
137
+ if (!resolvedApiKey)
138
+ return;
139
+ setRoutingStatus((prev) => (prev === "ready" ? prev : "loading"));
140
+ try {
141
+ const next = await fetchNonLocalizedPaths();
142
+ if (cancelled || !next)
143
+ return;
144
+ setNonLocalizedPaths(next);
145
+ setRoutingStatus("ready");
146
+ try {
147
+ localStorage.setItem(nonLocalizedStorageKey, JSON.stringify(next));
148
+ }
149
+ catch {
150
+ // ignore
151
+ }
152
+ }
153
+ catch (err) {
154
+ if (cancelled)
155
+ return;
156
+ setRoutingStatus("error");
157
+ logDebug("[Lovalingo] Failed to fetch non-localized paths:", err);
158
+ }
159
+ })();
160
+ return () => {
161
+ cancelled = true;
162
+ };
163
+ }, [fetchNonLocalizedPaths, nonLocalizedStorageKey, resolvedApiKey]);
80
164
  return (React.createElement(BrowserRouter, null,
81
165
  React.createElement(NavigateExporter, { navigateRef: navigateRef }),
82
- React.createElement(Routes, null,
83
- langs.map((lang) => (React.createElement(Route, { key: lang, path: `${lang}/*`, element: React.createElement(LangGuard, { defaultLang: defaultLang, lang: lang }) },
84
- React.createElement(Route, { index: true, element: React.createElement(React.Fragment, null, children) }),
85
- React.createElement(Route, { path: "*", element: React.createElement(React.Fragment, null, children) })))),
86
- React.createElement(Route, { path: "*", element: React.createElement(RedirectToDefaultLang, { defaultLang: defaultLang }, children) }))));
166
+ React.createElement(LangRoutingContext.Provider, { value: { nonLocalizedPaths, status: routingStatus } },
167
+ React.createElement(Routes, null,
168
+ langs.map((lang) => (React.createElement(Route, { key: lang, path: `${lang}/*`, element: React.createElement(LangGuard, { lang: lang, nonLocalizedPaths: nonLocalizedPaths }) },
169
+ React.createElement(Route, { index: true, element: React.createElement(React.Fragment, null, children) }),
170
+ React.createElement(Route, { path: "*", element: React.createElement(React.Fragment, null, children) })))),
171
+ React.createElement(Route, { path: "*", element: React.createElement(RedirectToDefaultLang, { defaultLang: defaultLang, nonLocalizedPaths: nonLocalizedPaths, routingStatus: routingStatus }, children) })))));
87
172
  }
@@ -0,0 +1,6 @@
1
+ import type { NonLocalizedPathRule } from "../utils/nonLocalizedPaths";
2
+ export type LangRoutingContextValue = {
3
+ nonLocalizedPaths: NonLocalizedPathRule[];
4
+ status: "unknown" | "loading" | "ready" | "error";
5
+ };
6
+ export declare const LangRoutingContext: import("react").Context<LangRoutingContextValue>;
@@ -0,0 +1,5 @@
1
+ import { createContext } from "react";
2
+ export const LangRoutingContext = createContext({
3
+ nonLocalizedPaths: [],
4
+ status: "unknown",
5
+ });
@@ -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
- //Globally excluded paths.
5
- const NON_LOCALIZED_APP_PATHS = new Set([
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,13 @@ 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.replace(/^\//, '');
34
- const firstSegment = trimmed.split('/')[0] || '';
35
- const fullPath = NON_LOCALIZED_APP_PATHS.has(firstSegment)
36
- ? `/${trimmed}`
37
- : `/${lang}/${trimmed}`;
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) ? normalized : `/${lang}${normalized}`;
38
36
  navigate(fullPath, options);
39
- }, [lang, navigate]);
37
+ }, [lang, navigate, routing.nonLocalizedPaths]);
40
38
  }
@@ -25,6 +25,11 @@ 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
+ }>;
28
33
  loading_bg_color?: string | null;
29
34
  branding_enabled?: boolean;
30
35
  seoEnabled?: boolean;
@@ -0,0 +1,9 @@
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[];
@@ -0,0 +1,78 @@
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.startsWith(pattern))
30
+ return true;
31
+ continue;
32
+ }
33
+ if (matchType === "regex") {
34
+ try {
35
+ if (new RegExp(pattern).test(input))
36
+ return true;
37
+ }
38
+ catch {
39
+ // ignore invalid regex rules
40
+ }
41
+ }
42
+ }
43
+ return false;
44
+ }
45
+ export function isNonLocalizedPath(pathname, rules) {
46
+ return isGlobalNonLocalizedPath(pathname) || matchesNonLocalizedRules(pathname, rules);
47
+ }
48
+ export function stripLocalePrefix(pathname, locales) {
49
+ const input = (pathname || "").toString();
50
+ if (!input.startsWith("/"))
51
+ return input;
52
+ const parts = input.split("/").filter(Boolean);
53
+ if (parts.length === 0)
54
+ return "/";
55
+ const first = parts[0] || "";
56
+ if (!first || !Array.isArray(locales) || !locales.includes(first))
57
+ return input;
58
+ const rest = `/${parts.slice(1).join("/")}`;
59
+ return rest === "" ? "/" : rest;
60
+ }
61
+ export function parseBootstrapNonLocalizedPaths(value) {
62
+ if (!Array.isArray(value))
63
+ return [];
64
+ const out = [];
65
+ for (const row of value) {
66
+ if (!row || typeof row !== "object")
67
+ continue;
68
+ const record = row;
69
+ const pattern = typeof record.pattern === "string" ? record.pattern.trim() : "";
70
+ const match_type = typeof record.match_type === "string" ? record.match_type.trim() : "";
71
+ if (!pattern)
72
+ continue;
73
+ if (match_type !== "exact" && match_type !== "prefix" && match_type !== "regex")
74
+ continue;
75
+ out.push({ pattern, match_type: match_type });
76
+ }
77
+ return out;
78
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.3.2";
1
+ export declare const VERSION = "0.5.3";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.3.2";
1
+ export const VERSION = "0.5.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovalingo/lovalingo",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
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",