@lovalingo/lovalingo 0.5.29 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/dist/chunk-2FZR2AKF.mjs +88 -0
- package/dist/chunk-7D5LBV45.mjs +46 -0
- package/dist/chunk-CJOSN7RA.mjs +90 -0
- package/dist/chunk-VAHA2TOX.mjs +3440 -0
- package/dist/chunk-ZMRCSUM7.mjs +26 -0
- package/dist/chunk-ZVYKEEUF.mjs +220 -0
- package/dist/core.d.mts +131 -0
- package/dist/core.d.ts +131 -0
- package/dist/core.js +3561 -0
- package/dist/core.mjs +19 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -25
- package/dist/index.js +3885 -28
- package/dist/index.mjs +33 -0
- package/dist/react-router.d.mts +101 -0
- package/dist/react-router.d.ts +101 -0
- package/dist/react-router.js +353 -0
- package/dist/react-router.mjs +14 -0
- package/dist/tanstack-router.d.mts +22 -0
- package/dist/tanstack-router.d.ts +22 -0
- package/dist/tanstack-router.js +162 -0
- package/dist/tanstack-router.mjs +8 -0
- package/package.json +34 -3
- package/dist/__tests__/languageFlags.test.d.ts +0 -1
- package/dist/__tests__/languageFlags.test.js +0 -42
- package/dist/__tests__/mergeEntitlements.test.d.ts +0 -1
- package/dist/__tests__/mergeEntitlements.test.js +0 -27
- package/dist/components/AixsterProvider.d.ts +0 -1
- package/dist/components/AixsterProvider.js +0 -1
- package/dist/components/LangLink.d.ts +0 -20
- package/dist/components/LangLink.js +0 -38
- package/dist/components/LangRouter.d.ts +0 -37
- package/dist/components/LangRouter.js +0 -191
- package/dist/components/LanguageSwitcher.d.ts +0 -17
- package/dist/components/LanguageSwitcher.js +0 -257
- package/dist/components/LovalingoProvider.d.ts +0 -10
- package/dist/components/LovalingoProvider.js +0 -413
- package/dist/components/NavigationOverlay.d.ts +0 -6
- package/dist/components/NavigationOverlay.js +0 -22
- package/dist/components/provider/__tests__/seoUtils.test.d.ts +0 -1
- package/dist/components/provider/__tests__/seoUtils.test.js +0 -13
- package/dist/components/provider/editModeUtils.d.ts +0 -6
- package/dist/components/provider/editModeUtils.js +0 -59
- package/dist/components/provider/localeUtils.d.ts +0 -8
- package/dist/components/provider/localeUtils.js +0 -46
- package/dist/components/provider/providerConstants.d.ts +0 -12
- package/dist/components/provider/providerConstants.js +0 -11
- package/dist/components/provider/seoUtils.d.ts +0 -8
- package/dist/components/provider/seoUtils.js +0 -118
- package/dist/components/provider/useEditModeOverlay.d.ts +0 -7
- package/dist/components/provider/useEditModeOverlay.js +0 -134
- package/dist/components/provider/useHistoryNavigationPatch.d.ts +0 -3
- package/dist/components/provider/useHistoryNavigationPatch.js +0 -47
- package/dist/components/provider/useProviderCache.d.ts +0 -12
- package/dist/components/provider/useProviderCache.js +0 -82
- package/dist/context/AixsterContext.d.ts +0 -3
- package/dist/context/AixsterContext.js +0 -2
- package/dist/context/LangContext.d.ts +0 -1
- package/dist/context/LangContext.js +0 -2
- package/dist/context/LangRoutingContext.d.ts +0 -8
- package/dist/context/LangRoutingContext.js +0 -7
- package/dist/context/LovalingoContext.d.ts +0 -1
- package/dist/context/LovalingoContext.js +0 -1
- package/dist/hooks/provider/useBundleLoading.d.ts +0 -33
- package/dist/hooks/provider/useBundleLoading.js +0 -380
- package/dist/hooks/provider/useDomRules.d.ts +0 -15
- package/dist/hooks/provider/useDomRules.js +0 -38
- package/dist/hooks/provider/useLinkAutoPrefix.d.ts +0 -12
- package/dist/hooks/provider/useLinkAutoPrefix.js +0 -146
- package/dist/hooks/provider/useNavigationPrefetch.d.ts +0 -12
- package/dist/hooks/provider/useNavigationPrefetch.js +0 -82
- package/dist/hooks/provider/usePageviewTracking.d.ts +0 -10
- package/dist/hooks/provider/usePageviewTracking.js +0 -44
- package/dist/hooks/provider/usePrehide.d.ts +0 -5
- package/dist/hooks/provider/usePrehide.js +0 -72
- package/dist/hooks/provider/useSitemapLinkTag.d.ts +0 -7
- package/dist/hooks/provider/useSitemapLinkTag.js +0 -28
- package/dist/hooks/provider/useStringMissReporting.d.ts +0 -14
- package/dist/hooks/provider/useStringMissReporting.js +0 -155
- package/dist/hooks/useAixster.d.ts +0 -6
- package/dist/hooks/useAixster.js +0 -14
- package/dist/hooks/useAixsterEdit.d.ts +0 -5
- package/dist/hooks/useAixsterEdit.js +0 -13
- package/dist/hooks/useAixsterTranslate.d.ts +0 -4
- package/dist/hooks/useAixsterTranslate.js +0 -12
- package/dist/hooks/useLang.d.ts +0 -16
- package/dist/hooks/useLang.js +0 -23
- package/dist/hooks/useLangNavigate.d.ts +0 -24
- package/dist/hooks/useLangNavigate.js +0 -40
- package/dist/hooks/useLovalingo.d.ts +0 -1
- package/dist/hooks/useLovalingo.js +0 -1
- package/dist/hooks/useLovalingoEdit.d.ts +0 -1
- package/dist/hooks/useLovalingoEdit.js +0 -1
- package/dist/hooks/useLovalingoTranslate.d.ts +0 -1
- package/dist/hooks/useLovalingoTranslate.js +0 -1
- package/dist/types.d.ts +0 -76
- package/dist/types.js +0 -1
- package/dist/utils/api.d.ts +0 -42
- package/dist/utils/api.js +0 -395
- package/dist/utils/apiTypes.d.ts +0 -78
- package/dist/utils/apiTypes.js +0 -1
- package/dist/utils/apiUtils.d.ts +0 -4
- package/dist/utils/apiUtils.js +0 -54
- package/dist/utils/domRules.d.ts +0 -2
- package/dist/utils/domRules.js +0 -150
- package/dist/utils/hash.d.ts +0 -9
- package/dist/utils/hash.js +0 -27
- package/dist/utils/languageFlags.d.ts +0 -7
- package/dist/utils/languageFlags.js +0 -90
- package/dist/utils/logger.d.ts +0 -3
- package/dist/utils/logger.js +0 -40
- package/dist/utils/markerEngine.d.ts +0 -12
- package/dist/utils/markerEngine.js +0 -109
- package/dist/utils/markerEngineApply.d.ts +0 -3
- package/dist/utils/markerEngineApply.js +0 -136
- package/dist/utils/markerEngineConstants.d.ts +0 -10
- package/dist/utils/markerEngineConstants.js +0 -12
- package/dist/utils/markerEngineCritical.d.ts +0 -2
- package/dist/utils/markerEngineCritical.js +0 -98
- package/dist/utils/markerEngineDomUtils.d.ts +0 -8
- package/dist/utils/markerEngineDomUtils.js +0 -74
- package/dist/utils/markerEngineFilters.d.ts +0 -2
- package/dist/utils/markerEngineFilters.js +0 -26
- package/dist/utils/markerEngineMisses.d.ts +0 -5
- package/dist/utils/markerEngineMisses.js +0 -81
- package/dist/utils/markerEngineOriginals.d.ts +0 -5
- package/dist/utils/markerEngineOriginals.js +0 -29
- package/dist/utils/markerEngineScan.d.ts +0 -5
- package/dist/utils/markerEngineScan.js +0 -162
- package/dist/utils/markerEngineState.d.ts +0 -4
- package/dist/utils/markerEngineState.js +0 -14
- package/dist/utils/markerEngineStats.d.ts +0 -3
- package/dist/utils/markerEngineStats.js +0 -28
- package/dist/utils/markerEngineTranslations.d.ts +0 -3
- package/dist/utils/markerEngineTranslations.js +0 -49
- package/dist/utils/markerEngineTypes.d.ts +0 -62
- package/dist/utils/markerEngineTypes.js +0 -1
- package/dist/utils/markerEngineViewport.d.ts +0 -2
- package/dist/utils/markerEngineViewport.js +0 -27
- package/dist/utils/mergeEntitlements.d.ts +0 -2
- package/dist/utils/mergeEntitlements.js +0 -7
- package/dist/utils/nonLocalizedPaths.d.ts +0 -12
- package/dist/utils/nonLocalizedPaths.js +0 -136
- package/dist/utils/pathNormalizer.d.ts +0 -49
- package/dist/utils/pathNormalizer.js +0 -115
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -1
|
@@ -1,413 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { LovalingoContext } from '../context/LovalingoContext';
|
|
3
|
-
import { LangRoutingContext } from '../context/LangRoutingContext';
|
|
4
|
-
import { LovalingoAPI } from '../utils/api';
|
|
5
|
-
import { applyActiveTranslations, setMarkerEngineExclusions, startMarkerEngine } from '../utils/markerEngine';
|
|
6
|
-
import { warnDebug } from '../utils/logger';
|
|
7
|
-
import { isNonLocalizedPath, stripLocalePrefix } from '../utils/nonLocalizedPaths';
|
|
8
|
-
import { processPath } from '../utils/pathNormalizer';
|
|
9
|
-
import { useBundleLoading } from '../hooks/provider/useBundleLoading';
|
|
10
|
-
import { useNavigationPrefetch } from '../hooks/provider/useNavigationPrefetch';
|
|
11
|
-
import { useLinkAutoPrefix } from '../hooks/provider/useLinkAutoPrefix';
|
|
12
|
-
import { usePageviewTracking } from '../hooks/provider/usePageviewTracking';
|
|
13
|
-
import { useSitemapLinkTag } from '../hooks/provider/useSitemapLinkTag';
|
|
14
|
-
import { useStringMissReporting } from '../hooks/provider/useStringMissReporting';
|
|
15
|
-
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
16
|
-
import { readEditParams } from './provider/editModeUtils';
|
|
17
|
-
import { applySeoBundle as applySeoBundleInternal } from './provider/seoUtils';
|
|
18
|
-
import { mergeEntitlementsSeoEnabled } from "../utils/mergeEntitlements";
|
|
19
|
-
import { useEditModeOverlay } from './provider/useEditModeOverlay';
|
|
20
|
-
import { useHistoryNavigationPatch } from './provider/useHistoryNavigationPatch';
|
|
21
|
-
import { detectLocaleFromLocation, setDocumentLocale } from './provider/localeUtils';
|
|
22
|
-
import { useProviderCache } from './provider/useProviderCache';
|
|
23
|
-
import { DEFAULT_PATH_NORMALIZATION, LIVE_MISSES_QUERY_PARAM, LOCALE_STORAGE_KEY } from './provider/providerConstants';
|
|
24
|
-
// Why: run initial load before first paint on the client to avoid a prehide flash; useEffect on SSR.
|
|
25
|
-
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
26
|
-
export const LovalingoProvider = ({ children, apiKey: apiKeyProp, publicAnonKey, defaultLocale, locales, apiBase = 'https://cdn.lovalingo.com', routing = 'path', // Default to path mode (SEO-friendly, recommended)
|
|
27
|
-
autoPrefixLinks = true, overlayBgColor, autoApplyRules = true, switcherPosition = 'bottom-right', switcherOffsetY = 20, switcherTheme = 'dark', editMode: initialEditMode = false, editKey = 'KeyE', pathNormalization = DEFAULT_PATH_NORMALIZATION, // Enable by default
|
|
28
|
-
mode = 'dom', // Default to legacy DOM mode for backward compatibility
|
|
29
|
-
sitemap = true, // Default: true - Auto-inject sitemap link tag
|
|
30
|
-
seo = true, // Default: true - Can be disabled per project entitlements
|
|
31
|
-
navigateRef, // For path mode routing
|
|
32
|
-
}) => {
|
|
33
|
-
const metaKey = typeof document !== "undefined"
|
|
34
|
-
? document.querySelector('meta[name="lovalingo-public-anon-key"]')?.content?.trim() || ""
|
|
35
|
-
: "";
|
|
36
|
-
const resolvedApiKey = (typeof apiKeyProp === "string" && apiKeyProp.trim().length > 0
|
|
37
|
-
? apiKeyProp
|
|
38
|
-
: typeof publicAnonKey === "string" && publicAnonKey.trim().length > 0
|
|
39
|
-
? publicAnonKey
|
|
40
|
-
: globalThis
|
|
41
|
-
.__LOVALINGO_PUBLIC_ANON_KEY__ ||
|
|
42
|
-
globalThis.__LOVALINGO_API_KEY__ ||
|
|
43
|
-
metaKey ||
|
|
44
|
-
"");
|
|
45
|
-
const rawLocales = Array.isArray(locales) ? locales : [];
|
|
46
|
-
// Stabilize locale lists even when callers pass inline arrays (e.g. locales={["en","de"]})
|
|
47
|
-
// so effects/callbacks don't re-run every render.
|
|
48
|
-
const localesKey = rawLocales.join(",");
|
|
49
|
-
const allLocales = useMemo(() => {
|
|
50
|
-
const base = rawLocales.includes(defaultLocale) ? rawLocales : [defaultLocale, ...rawLocales];
|
|
51
|
-
return Array.from(new Set(base));
|
|
52
|
-
}, [defaultLocale, localesKey]);
|
|
53
|
-
const pathNormalizationKey = (() => {
|
|
54
|
-
const enabled = pathNormalization?.enabled !== false;
|
|
55
|
-
const rules = Array.isArray(pathNormalization?.rules) ? pathNormalization.rules : [];
|
|
56
|
-
// Why: callers often pass inline objects/arrays; derive a stable key to prevent request storms from re-initializing effects.
|
|
57
|
-
const rulesKey = rules.map((r) => `${r.pattern}=>${r.replacement}:${r.includeSubpaths ? 1 : 0}`).join("|");
|
|
58
|
-
return `${enabled ? 1 : 0}:${rulesKey}`;
|
|
59
|
-
})();
|
|
60
|
-
const stablePathNormalization = useMemo(() => {
|
|
61
|
-
const enabled = pathNormalization?.enabled !== false;
|
|
62
|
-
const rules = Array.isArray(pathNormalization?.rules) ? pathNormalization.rules : undefined;
|
|
63
|
-
return rules ? { enabled, rules } : { enabled };
|
|
64
|
-
}, [pathNormalizationKey]);
|
|
65
|
-
// Why: read locale synchronously from the URL to avoid an initial default-locale render (EN -> FR flash) before effects run.
|
|
66
|
-
const [locale, setLocaleState] = useState(() => {
|
|
67
|
-
if (typeof window === "undefined")
|
|
68
|
-
return defaultLocale;
|
|
69
|
-
if (routing === "path") {
|
|
70
|
-
const pathLocale = window.location.pathname.split("/")[1];
|
|
71
|
-
if (pathLocale && allLocales.includes(pathLocale)) {
|
|
72
|
-
return pathLocale;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
else if (routing === "query") {
|
|
76
|
-
const params = new URLSearchParams(window.location.search);
|
|
77
|
-
const queryLocale = params.get("t") || params.get("locale");
|
|
78
|
-
if (queryLocale && allLocales.includes(queryLocale)) {
|
|
79
|
-
return queryLocale;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
try {
|
|
83
|
-
const stored = localStorage.getItem(LOCALE_STORAGE_KEY);
|
|
84
|
-
if (stored && allLocales.includes(stored)) {
|
|
85
|
-
return stored;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
// ignore
|
|
90
|
-
}
|
|
91
|
-
return defaultLocale;
|
|
92
|
-
});
|
|
93
|
-
const initialEditParams = readEditParams();
|
|
94
|
-
const [editMode, setEditMode] = useState(initialEditMode || initialEditParams.enabled);
|
|
95
|
-
const [editSecretKey] = useState(initialEditParams.editKey);
|
|
96
|
-
const enhancedPathConfig = useMemo(() => (routing === "path" ? { ...stablePathNormalization, supportedLocales: allLocales } : stablePathNormalization), [allLocales, routing, stablePathNormalization]);
|
|
97
|
-
const apiRef = useRef(new LovalingoAPI(resolvedApiKey, apiBase, enhancedPathConfig, editSecretKey ?? undefined));
|
|
98
|
-
useEffect(() => {
|
|
99
|
-
apiRef.current = new LovalingoAPI(resolvedApiKey, apiBase, enhancedPathConfig, editSecretKey ?? undefined);
|
|
100
|
-
}, [apiBase, editSecretKey, enhancedPathConfig, resolvedApiKey]);
|
|
101
|
-
const routingConfig = useContext(LangRoutingContext);
|
|
102
|
-
const [entitlements, setEntitlements] = useState(() => apiRef.current.getEntitlements());
|
|
103
|
-
const { trackPageviewOnce } = usePageviewTracking({ apiRef, resolvedApiKey });
|
|
104
|
-
const lastNormalizedPathRef = useRef("");
|
|
105
|
-
const onNavigateRef = useRef(() => undefined);
|
|
106
|
-
const isInternalNavigationRef = useRef(false);
|
|
107
|
-
const { brandingEnabled, setBrandingEnabled, setCachedBrandingEnabled, getCachedLoadingBgColor, setCachedLoadingBgColor, } = useProviderCache({ overlayBgColor, resolvedApiKey });
|
|
108
|
-
const config = {
|
|
109
|
-
apiKey: resolvedApiKey,
|
|
110
|
-
publicAnonKey: resolvedApiKey,
|
|
111
|
-
defaultLocale,
|
|
112
|
-
locales: allLocales,
|
|
113
|
-
apiBase,
|
|
114
|
-
routing,
|
|
115
|
-
autoPrefixLinks,
|
|
116
|
-
overlayBgColor,
|
|
117
|
-
switcherPosition,
|
|
118
|
-
switcherOffsetY,
|
|
119
|
-
switcherTheme,
|
|
120
|
-
editMode: initialEditMode,
|
|
121
|
-
editKey,
|
|
122
|
-
pathNormalization,
|
|
123
|
-
mode,
|
|
124
|
-
autoApplyRules,
|
|
125
|
-
};
|
|
126
|
-
const isSeoActive = useCallback(() => {
|
|
127
|
-
// Prop can force-disable SEO; server can also disable per project.
|
|
128
|
-
const serverEnabled = entitlements?.seoEnabled;
|
|
129
|
-
if (serverEnabled === false)
|
|
130
|
-
return false;
|
|
131
|
-
return seo !== false;
|
|
132
|
-
}, [entitlements, seo]);
|
|
133
|
-
// Marker engine: always mark full DOM content for deterministic pipeline extraction.
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
const stop = startMarkerEngine({ throttleMs: 120 });
|
|
136
|
-
return () => stop();
|
|
137
|
-
}, []);
|
|
138
|
-
// Detect locale from URL or localStorage
|
|
139
|
-
const detectLocale = useCallback(() => detectLocaleFromLocation({ routing, allLocales, defaultLocale }), [allLocales, defaultLocale, routing]);
|
|
140
|
-
// Fetch entitlements early so SEO can be enabled even on default locale
|
|
141
|
-
useEffect(() => {
|
|
142
|
-
if (locale !== defaultLocale)
|
|
143
|
-
return;
|
|
144
|
-
if (entitlements)
|
|
145
|
-
return;
|
|
146
|
-
let cancelled = false;
|
|
147
|
-
(async () => {
|
|
148
|
-
const bootstrap = await apiRef.current.fetchBootstrap(locale, window.location.pathname + window.location.search);
|
|
149
|
-
if (cancelled)
|
|
150
|
-
return;
|
|
151
|
-
if (bootstrap?.entitlements) {
|
|
152
|
-
setEntitlements(mergeEntitlementsSeoEnabled(bootstrap.entitlements, bootstrap.seoEnabled));
|
|
153
|
-
}
|
|
154
|
-
if (bootstrap?.loading_bg_color)
|
|
155
|
-
setCachedLoadingBgColor(bootstrap.loading_bg_color);
|
|
156
|
-
if (bootstrap?.entitlements?.brandingRequired) {
|
|
157
|
-
setBrandingEnabled(true);
|
|
158
|
-
setCachedBrandingEnabled(true);
|
|
159
|
-
}
|
|
160
|
-
else if (typeof bootstrap?.branding_enabled === "boolean") {
|
|
161
|
-
setBrandingEnabled(bootstrap.branding_enabled);
|
|
162
|
-
setCachedBrandingEnabled(bootstrap.branding_enabled);
|
|
163
|
-
}
|
|
164
|
-
})();
|
|
165
|
-
return () => {
|
|
166
|
-
cancelled = true;
|
|
167
|
-
};
|
|
168
|
-
}, [defaultLocale, entitlements, locale, setCachedBrandingEnabled, setCachedLoadingBgColor]);
|
|
169
|
-
const applySeoBundle = useCallback((bundle, hreflangEnabled) => {
|
|
170
|
-
applySeoBundleInternal(bundle, hreflangEnabled);
|
|
171
|
-
}, []);
|
|
172
|
-
// Keep <html lang> in sync and apply default-locale SEO (non-default locales use the bootstrap payload).
|
|
173
|
-
useEffect(() => {
|
|
174
|
-
setDocumentLocale(locale);
|
|
175
|
-
if (locale !== defaultLocale)
|
|
176
|
-
return;
|
|
177
|
-
if (!isSeoActive())
|
|
178
|
-
return;
|
|
179
|
-
void apiRef.current.fetchSeoBundle(locale).then((bundle) => {
|
|
180
|
-
applySeoBundle(bundle, Boolean(entitlements?.hreflangEnabled));
|
|
181
|
-
});
|
|
182
|
-
}, [applySeoBundle, defaultLocale, entitlements, isSeoActive, locale, setDocumentLocale]);
|
|
183
|
-
const { isLoading, isNavigatingRef, loadData } = useBundleLoading({
|
|
184
|
-
apiRef,
|
|
185
|
-
resolvedApiKey,
|
|
186
|
-
defaultLocale,
|
|
187
|
-
routing,
|
|
188
|
-
allLocales,
|
|
189
|
-
nonLocalizedPaths: routingConfig.nonLocalizedPaths,
|
|
190
|
-
enhancedPathConfig,
|
|
191
|
-
mode,
|
|
192
|
-
autoApplyRules,
|
|
193
|
-
seoProp: seo,
|
|
194
|
-
isSeoActive,
|
|
195
|
-
applySeoBundle,
|
|
196
|
-
setEntitlements,
|
|
197
|
-
setBrandingEnabled,
|
|
198
|
-
setCachedBrandingEnabled,
|
|
199
|
-
setCachedLoadingBgColor,
|
|
200
|
-
getCachedLoadingBgColor,
|
|
201
|
-
});
|
|
202
|
-
useEffect(() => {
|
|
203
|
-
onNavigateRef.current = () => {
|
|
204
|
-
trackPageviewOnce(window.location.pathname + window.location.search);
|
|
205
|
-
const nextLocale = detectLocale();
|
|
206
|
-
const normalizedPath = processPath(window.location.pathname, enhancedPathConfig);
|
|
207
|
-
const normalizedPathChanged = normalizedPath !== lastNormalizedPathRef.current;
|
|
208
|
-
lastNormalizedPathRef.current = normalizedPath;
|
|
209
|
-
// Why: bundles are path-scoped, so SPA navigations within the same locale must trigger a reload for the new route.
|
|
210
|
-
if (normalizedPathChanged && nextLocale !== defaultLocale && !isInternalNavigationRef.current) {
|
|
211
|
-
void loadData(nextLocale, locale);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
if (nextLocale !== locale) {
|
|
215
|
-
setLocaleState(nextLocale);
|
|
216
|
-
if (!isInternalNavigationRef.current) {
|
|
217
|
-
void loadData(nextLocale, locale);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
else if (mode === "dom" && nextLocale !== defaultLocale) {
|
|
221
|
-
applyActiveTranslations(document.body);
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
}, [defaultLocale, detectLocale, enhancedPathConfig, loadData, locale, mode, trackPageviewOnce]);
|
|
225
|
-
useHistoryNavigationPatch(onNavigateRef);
|
|
226
|
-
// Change locale
|
|
227
|
-
const setLocale = useCallback((newLocale) => {
|
|
228
|
-
void (async () => {
|
|
229
|
-
if (!allLocales.includes(newLocale))
|
|
230
|
-
return;
|
|
231
|
-
// Save to localStorage
|
|
232
|
-
try {
|
|
233
|
-
localStorage.setItem(LOCALE_STORAGE_KEY, newLocale);
|
|
234
|
-
}
|
|
235
|
-
catch (e) {
|
|
236
|
-
warnDebug('Failed to save locale to localStorage:', e);
|
|
237
|
-
}
|
|
238
|
-
isInternalNavigationRef.current = true;
|
|
239
|
-
// Prevent MutationObserver work during the switch to avoid React conflicts
|
|
240
|
-
isNavigatingRef.current = true;
|
|
241
|
-
// Why: force a full reload on locale switches to avoid mixed-language DOM residues.
|
|
242
|
-
let nextUrl = "";
|
|
243
|
-
// Update URL based on routing strategy
|
|
244
|
-
if (routing === 'path') {
|
|
245
|
-
const stripped = stripLocalePrefix(window.location.pathname, allLocales);
|
|
246
|
-
if (isNonLocalizedPath(stripped, routingConfig.nonLocalizedPaths)) {
|
|
247
|
-
// Why: switching languages must not rewrite non-localized routes like "/auth" to "/de/auth".
|
|
248
|
-
nextUrl = window.location.href;
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
const pathParts = window.location.pathname.split('/').filter(Boolean);
|
|
252
|
-
// Strip existing locale
|
|
253
|
-
if (allLocales.includes(pathParts[0])) {
|
|
254
|
-
pathParts.shift();
|
|
255
|
-
}
|
|
256
|
-
// Build new path with new locale
|
|
257
|
-
const basePath = pathParts.join('/');
|
|
258
|
-
nextUrl = `/${newLocale}${basePath ? '/' + basePath : ''}${window.location.search}${window.location.hash}`;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
else if (routing === 'query') {
|
|
262
|
-
const url = new URL(window.location.href);
|
|
263
|
-
url.searchParams.set('t', newLocale);
|
|
264
|
-
nextUrl = url.toString();
|
|
265
|
-
}
|
|
266
|
-
if (!nextUrl)
|
|
267
|
-
nextUrl = window.location.href;
|
|
268
|
-
window.location.assign(nextUrl);
|
|
269
|
-
return;
|
|
270
|
-
})().finally(() => {
|
|
271
|
-
isInternalNavigationRef.current = false;
|
|
272
|
-
});
|
|
273
|
-
}, [allLocales, locale, routing, routingConfig.nonLocalizedPaths]);
|
|
274
|
-
// No string-level "misses" from the client: the pipeline (Browser Rendering) is source-of-truth for string discovery.
|
|
275
|
-
// Why: prevent init/load effects from re-running (and calling bootstrap/bundle again) when loadData changes due to state updates.
|
|
276
|
-
const loadDataRef = useRef(loadData);
|
|
277
|
-
useEffect(() => {
|
|
278
|
-
loadDataRef.current = loadData;
|
|
279
|
-
}, [loadData]);
|
|
280
|
-
const detectLocaleRef = useRef(detectLocale);
|
|
281
|
-
useEffect(() => {
|
|
282
|
-
detectLocaleRef.current = detectLocale;
|
|
283
|
-
}, [detectLocale]);
|
|
284
|
-
useEffect(() => {
|
|
285
|
-
if (editMode && !editSecretKey) {
|
|
286
|
-
warnDebug('[Lovalingo] Edit Mode is active but no edit_key was provided in the URL.');
|
|
287
|
-
}
|
|
288
|
-
}, [editMode, editSecretKey]);
|
|
289
|
-
// Initialize
|
|
290
|
-
useIsomorphicLayoutEffect(() => {
|
|
291
|
-
const applyLiveMissesQueryParam = () => {
|
|
292
|
-
if (typeof window === "undefined")
|
|
293
|
-
return;
|
|
294
|
-
const url = new URL(window.location.href);
|
|
295
|
-
const raw = url.searchParams.get(LIVE_MISSES_QUERY_PARAM);
|
|
296
|
-
if (raw !== "0" && raw !== "1")
|
|
297
|
-
return;
|
|
298
|
-
const g = window;
|
|
299
|
-
if (raw === "1") {
|
|
300
|
-
if (g.__lovalingoDisableMisses === true)
|
|
301
|
-
return;
|
|
302
|
-
g.__lovalingoDisableMisses = false;
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
g.__lovalingoDisableMisses = true;
|
|
306
|
-
};
|
|
307
|
-
applyLiveMissesQueryParam();
|
|
308
|
-
const initialLocale = detectLocaleRef.current();
|
|
309
|
-
lastNormalizedPathRef.current = processPath(window.location.pathname, enhancedPathConfig);
|
|
310
|
-
// Track initial page (fallback discovery for pages not present in the routes feed).
|
|
311
|
-
trackPageviewOnce(window.location.pathname + window.location.search);
|
|
312
|
-
// Always prefetch artifacts for the initial locale (pipeline-produced translations + rules).
|
|
313
|
-
loadDataRef.current(initialLocale);
|
|
314
|
-
// Set up keyboard shortcut for edit mode
|
|
315
|
-
const handleKeyPress = (e) => {
|
|
316
|
-
if (e.code === editKey && (e.ctrlKey || e.metaKey)) {
|
|
317
|
-
e.preventDefault();
|
|
318
|
-
setEditMode(prev => !prev);
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
|
-
window.addEventListener('keydown', handleKeyPress);
|
|
322
|
-
return () => {
|
|
323
|
-
window.removeEventListener('keydown', handleKeyPress);
|
|
324
|
-
};
|
|
325
|
-
}, [editKey, enhancedPathConfig, trackPageviewOnce]);
|
|
326
|
-
useSitemapLinkTag({ enabled: sitemap, resolvedApiKey, isSeoActive });
|
|
327
|
-
useLinkAutoPrefix({
|
|
328
|
-
routing,
|
|
329
|
-
autoPrefixLinks,
|
|
330
|
-
allLocales,
|
|
331
|
-
locale,
|
|
332
|
-
navigateRef,
|
|
333
|
-
nonLocalizedPaths: routingConfig.nonLocalizedPaths,
|
|
334
|
-
});
|
|
335
|
-
useNavigationPrefetch({
|
|
336
|
-
resolvedApiKey,
|
|
337
|
-
apiBase,
|
|
338
|
-
defaultLocale,
|
|
339
|
-
locale,
|
|
340
|
-
routing,
|
|
341
|
-
allLocales,
|
|
342
|
-
enhancedPathConfig,
|
|
343
|
-
});
|
|
344
|
-
useStringMissReporting({
|
|
345
|
-
apiRef,
|
|
346
|
-
resolvedApiKey,
|
|
347
|
-
locale,
|
|
348
|
-
defaultLocale,
|
|
349
|
-
routing,
|
|
350
|
-
allLocales,
|
|
351
|
-
nonLocalizedPaths: routingConfig.nonLocalizedPaths,
|
|
352
|
-
isLoading,
|
|
353
|
-
mode,
|
|
354
|
-
});
|
|
355
|
-
// Dynamic DOM updates are handled by the marker engine observer (React-safe in-place text/attr updates).
|
|
356
|
-
// No periodic string-miss reporting. Page discovery is tracked via pageview only.
|
|
357
|
-
const translateElement = useCallback((element) => {
|
|
358
|
-
if (mode !== "dom")
|
|
359
|
-
return;
|
|
360
|
-
applyActiveTranslations(element);
|
|
361
|
-
}, []);
|
|
362
|
-
const translateDOM = useCallback(() => {
|
|
363
|
-
if (mode !== "dom")
|
|
364
|
-
return;
|
|
365
|
-
applyActiveTranslations(document.body);
|
|
366
|
-
}, []);
|
|
367
|
-
const toggleEditMode = useCallback(() => {
|
|
368
|
-
setEditMode(prev => !prev);
|
|
369
|
-
}, []);
|
|
370
|
-
const excludeElement = useCallback(async (selector) => {
|
|
371
|
-
if (!editSecretKey) {
|
|
372
|
-
warnDebug('[Lovalingo] Edit Mode is active but no edit_key was provided in the URL.');
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
// Why: store exclusions on the normalized path so they apply across locales.
|
|
376
|
-
const pagePath = lastNormalizedPathRef.current || processPath(window.location.pathname, enhancedPathConfig);
|
|
377
|
-
await apiRef.current.saveExclusion({
|
|
378
|
-
selector,
|
|
379
|
-
type: 'css',
|
|
380
|
-
pagePath,
|
|
381
|
-
editKey: editSecretKey,
|
|
382
|
-
});
|
|
383
|
-
const exclusions = await apiRef.current.fetchExclusions();
|
|
384
|
-
setMarkerEngineExclusions(exclusions);
|
|
385
|
-
}, [editSecretKey, enhancedPathConfig]);
|
|
386
|
-
useEditModeOverlay({ editMode, excludeElement, setEditMode });
|
|
387
|
-
const contextValue = {
|
|
388
|
-
locale,
|
|
389
|
-
setLocale,
|
|
390
|
-
isLoading,
|
|
391
|
-
translationMap: {},
|
|
392
|
-
config,
|
|
393
|
-
translateElement,
|
|
394
|
-
translateDOM,
|
|
395
|
-
editMode,
|
|
396
|
-
toggleEditMode,
|
|
397
|
-
excludeElement,
|
|
398
|
-
};
|
|
399
|
-
return (React.createElement(LovalingoContext.Provider, { value: contextValue },
|
|
400
|
-
children,
|
|
401
|
-
(() => {
|
|
402
|
-
if (routing !== "path")
|
|
403
|
-
return true;
|
|
404
|
-
if (typeof window === "undefined")
|
|
405
|
-
return true;
|
|
406
|
-
const stripped = stripLocalePrefix(window.location.pathname, allLocales);
|
|
407
|
-
return !isNonLocalizedPath(stripped, routingConfig.nonLocalizedPaths);
|
|
408
|
-
})() && (React.createElement(LanguageSwitcher, { locales: allLocales, currentLocale: locale, onLocaleChange: setLocale, position: switcherPosition, offsetY: switcherOffsetY, theme: switcherTheme, branding: {
|
|
409
|
-
required: Boolean(entitlements?.brandingRequired),
|
|
410
|
-
enabled: brandingEnabled,
|
|
411
|
-
href: "https://lovalingo.com",
|
|
412
|
-
} }))));
|
|
413
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
export const NavigationOverlay = ({ isVisible }) => {
|
|
3
|
-
if (!isVisible)
|
|
4
|
-
return null;
|
|
5
|
-
return (React.createElement("div", { style: {
|
|
6
|
-
position: 'fixed',
|
|
7
|
-
top: 0,
|
|
8
|
-
left: 0,
|
|
9
|
-
right: 0,
|
|
10
|
-
bottom: 0,
|
|
11
|
-
backgroundColor: 'rgba(255, 255, 255, 0.01)',
|
|
12
|
-
zIndex: 9999,
|
|
13
|
-
animation: 'fadeIn 0.1s ease-in-out',
|
|
14
|
-
cursor: 'progress',
|
|
15
|
-
}, "data-Lovalingo-exclude": "true" },
|
|
16
|
-
React.createElement("style", null, `
|
|
17
|
-
@keyframes fadeIn {
|
|
18
|
-
from { opacity: 0; }
|
|
19
|
-
to { opacity: 1; }
|
|
20
|
-
}
|
|
21
|
-
`)));
|
|
22
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { resolveCanonicalHref } from "../seoUtils";
|
|
3
|
-
describe("resolveCanonicalHref", () => {
|
|
4
|
-
test("prefers alternates canonical when present", () => {
|
|
5
|
-
expect(resolveCanonicalHref({ canonical_url: "https://example.com/en" }, { canonical: "https://example.com/fr" })).toBe("https://example.com/fr");
|
|
6
|
-
});
|
|
7
|
-
test("falls back to seo.canonical_url", () => {
|
|
8
|
-
expect(resolveCanonicalHref({ canonical_url: "https://example.com/en" }, null)).toBe("https://example.com/en");
|
|
9
|
-
});
|
|
10
|
-
test("returns empty when none", () => {
|
|
11
|
-
expect(resolveCanonicalHref({}, null)).toBe("");
|
|
12
|
-
});
|
|
13
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { EDIT_KEY_PARAM, EDIT_MODE_PARAM, EDIT_MODE_VALUES } from './providerConstants';
|
|
2
|
-
export function readEditParams() {
|
|
3
|
-
if (typeof window === 'undefined')
|
|
4
|
-
return { enabled: false, editKey: null };
|
|
5
|
-
const params = new URLSearchParams(window.location.search);
|
|
6
|
-
const rawFlag = (params.get(EDIT_MODE_PARAM) || params.get('editMode') || '').trim().toLowerCase();
|
|
7
|
-
const enabled = EDIT_MODE_VALUES.has(rawFlag);
|
|
8
|
-
const editKey = (params.get(EDIT_KEY_PARAM) || params.get('editKey') || '').trim() || null;
|
|
9
|
-
return { enabled, editKey };
|
|
10
|
-
}
|
|
11
|
-
export function cssEscape(value) {
|
|
12
|
-
const esc = typeof window !== 'undefined' && window?.CSS?.escape;
|
|
13
|
-
if (typeof esc === 'function')
|
|
14
|
-
return esc(value);
|
|
15
|
-
return value.replace(/[ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, '\\$&');
|
|
16
|
-
}
|
|
17
|
-
// Why: build stable, short selectors for exclusions without requiring IDs.
|
|
18
|
-
export function buildCssSelector(element, maxDepth = 5) {
|
|
19
|
-
if (!element || !element.tagName)
|
|
20
|
-
return null;
|
|
21
|
-
if (element.id)
|
|
22
|
-
return `#${cssEscape(element.id)}`;
|
|
23
|
-
const parts = [];
|
|
24
|
-
let node = element;
|
|
25
|
-
let depth = 0;
|
|
26
|
-
while (node && depth < maxDepth) {
|
|
27
|
-
const tag = node.tagName.toLowerCase();
|
|
28
|
-
if (!tag || tag === 'html')
|
|
29
|
-
break;
|
|
30
|
-
let part = tag;
|
|
31
|
-
const nodeTag = node.tagName;
|
|
32
|
-
const classes = Array.from(node.classList || [])
|
|
33
|
-
.filter(Boolean)
|
|
34
|
-
.filter((cls) => !cls.startsWith('lovalingo-'))
|
|
35
|
-
.slice(0, 2);
|
|
36
|
-
if (classes.length > 0) {
|
|
37
|
-
part += `.${classes.map(cssEscape).join('.')}`;
|
|
38
|
-
}
|
|
39
|
-
const parentEl = node.parentElement;
|
|
40
|
-
if (parentEl) {
|
|
41
|
-
const siblings = Array.from(parentEl.children).filter((child) => child instanceof HTMLElement && child.tagName === nodeTag);
|
|
42
|
-
if (siblings.length > 1) {
|
|
43
|
-
part += `:nth-of-type(${siblings.indexOf(node) + 1})`;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
parts.unshift(part);
|
|
47
|
-
const selector = parts.join(' > ');
|
|
48
|
-
try {
|
|
49
|
-
if (document.querySelectorAll(selector).length === 1)
|
|
50
|
-
return selector;
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
// ignore invalid selector attempts
|
|
54
|
-
}
|
|
55
|
-
node = parentEl;
|
|
56
|
-
depth += 1;
|
|
57
|
-
}
|
|
58
|
-
return parts.join(' > ') || null;
|
|
59
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
type DetectLocaleArgs = {
|
|
2
|
-
routing: 'path' | 'query';
|
|
3
|
-
allLocales: string[];
|
|
4
|
-
defaultLocale: string;
|
|
5
|
-
};
|
|
6
|
-
export declare function detectLocaleFromLocation({ routing, allLocales, defaultLocale }: DetectLocaleArgs): string;
|
|
7
|
-
export declare function setDocumentLocale(nextLocale: string): void;
|
|
8
|
-
export {};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { warnDebug } from '../../utils/logger';
|
|
2
|
-
import { LOCALE_STORAGE_KEY } from './providerConstants';
|
|
3
|
-
export function detectLocaleFromLocation({ routing, allLocales, defaultLocale }) {
|
|
4
|
-
// 1. Check URL first based on routing mode
|
|
5
|
-
if (routing === 'path') {
|
|
6
|
-
// Path mode: language is in path (/en/pricing, /fr/about)
|
|
7
|
-
const pathLocale = window.location.pathname.split('/')[1];
|
|
8
|
-
if (pathLocale && allLocales.includes(pathLocale)) {
|
|
9
|
-
return pathLocale;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
else if (routing === 'query') {
|
|
13
|
-
// Query mode: language is in query param (/pricing?t=fr)
|
|
14
|
-
const params = new URLSearchParams(window.location.search);
|
|
15
|
-
const queryLocale = params.get('t') || params.get('locale');
|
|
16
|
-
if (queryLocale && allLocales.includes(queryLocale)) {
|
|
17
|
-
return queryLocale;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
// 2. Check localStorage (fallback for all routing modes)
|
|
21
|
-
try {
|
|
22
|
-
const storedLocale = localStorage.getItem(LOCALE_STORAGE_KEY);
|
|
23
|
-
if (storedLocale && allLocales.includes(storedLocale)) {
|
|
24
|
-
return storedLocale;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
catch (e) {
|
|
28
|
-
// localStorage might be unavailable (SSR, private browsing)
|
|
29
|
-
warnDebug('localStorage not available:', e);
|
|
30
|
-
}
|
|
31
|
-
// 3. Default locale
|
|
32
|
-
return defaultLocale;
|
|
33
|
-
}
|
|
34
|
-
export function setDocumentLocale(nextLocale) {
|
|
35
|
-
try {
|
|
36
|
-
const html = document.documentElement;
|
|
37
|
-
if (!html)
|
|
38
|
-
return;
|
|
39
|
-
html.setAttribute('lang', nextLocale);
|
|
40
|
-
const rtlLocales = new Set(['ar', 'he', 'fa', 'ur']);
|
|
41
|
-
html.setAttribute('dir', rtlLocales.has(nextLocale) ? 'rtl' : 'ltr');
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// ignore
|
|
45
|
-
}
|
|
46
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { PathNormalizationConfig } from '../../utils/pathNormalizer';
|
|
2
|
-
export declare const LOCALE_STORAGE_KEY = "Lovalingo_locale";
|
|
3
|
-
export declare const LOADING_BG_STORAGE_PREFIX = "Lovalingo_loading_bg_color";
|
|
4
|
-
export declare const BRANDING_STORAGE_PREFIX = "Lovalingo_branding_enabled";
|
|
5
|
-
export declare const EDIT_MODE_PARAM = "edit_mode";
|
|
6
|
-
export declare const EDIT_KEY_PARAM = "edit_key";
|
|
7
|
-
export declare const LIVE_MISSES_QUERY_PARAM = "lovalingo_live_misses";
|
|
8
|
-
export declare const DEFAULT_PATH_NORMALIZATION: PathNormalizationConfig;
|
|
9
|
-
export declare const EDIT_MODE_VALUES: Set<string>;
|
|
10
|
-
export declare const EDIT_UI_ATTR = "data-lovalingo-edit-ui";
|
|
11
|
-
export declare const EDIT_HIGHLIGHT_ID = "lovalingo-edit-highlight";
|
|
12
|
-
export declare const EDIT_HINT_ID = "lovalingo-edit-hint";
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export const LOCALE_STORAGE_KEY = 'Lovalingo_locale';
|
|
2
|
-
export const LOADING_BG_STORAGE_PREFIX = 'Lovalingo_loading_bg_color';
|
|
3
|
-
export const BRANDING_STORAGE_PREFIX = 'Lovalingo_branding_enabled';
|
|
4
|
-
export const EDIT_MODE_PARAM = 'edit_mode';
|
|
5
|
-
export const EDIT_KEY_PARAM = 'edit_key';
|
|
6
|
-
export const LIVE_MISSES_QUERY_PARAM = 'lovalingo_live_misses';
|
|
7
|
-
export const DEFAULT_PATH_NORMALIZATION = { enabled: true };
|
|
8
|
-
export const EDIT_MODE_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
9
|
-
export const EDIT_UI_ATTR = 'data-lovalingo-edit-ui';
|
|
10
|
-
export const EDIT_HIGHLIGHT_ID = 'lovalingo-edit-highlight';
|
|
11
|
-
export const EDIT_HINT_ID = 'lovalingo-edit-hint';
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export declare function applySeoBundle(bundle: {
|
|
2
|
-
seo?: Record<string, unknown>;
|
|
3
|
-
alternates?: any;
|
|
4
|
-
jsonld?: any;
|
|
5
|
-
} | null, hreflangEnabled: boolean): void;
|
|
6
|
-
export declare function resolveCanonicalHref(seo: Record<string, unknown>, alternates: {
|
|
7
|
-
canonical?: string;
|
|
8
|
-
} | null | undefined): string;
|