@lovalingo/lovalingo 0.5.28 → 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.
Files changed (148) hide show
  1. package/README.md +36 -0
  2. package/dist/chunk-2FZR2AKF.mjs +88 -0
  3. package/dist/chunk-7D5LBV45.mjs +46 -0
  4. package/dist/chunk-CJOSN7RA.mjs +90 -0
  5. package/dist/chunk-VAHA2TOX.mjs +3440 -0
  6. package/dist/chunk-ZMRCSUM7.mjs +26 -0
  7. package/dist/chunk-ZVYKEEUF.mjs +220 -0
  8. package/dist/core.d.mts +131 -0
  9. package/dist/core.d.ts +131 -0
  10. package/dist/core.js +3561 -0
  11. package/dist/core.mjs +19 -0
  12. package/dist/index.d.mts +5 -0
  13. package/dist/index.d.ts +5 -25
  14. package/dist/index.js +3885 -28
  15. package/dist/index.mjs +33 -0
  16. package/dist/react-router.d.mts +101 -0
  17. package/dist/react-router.d.ts +101 -0
  18. package/dist/react-router.js +353 -0
  19. package/dist/react-router.mjs +14 -0
  20. package/dist/tanstack-router.d.mts +22 -0
  21. package/dist/tanstack-router.d.ts +22 -0
  22. package/dist/tanstack-router.js +162 -0
  23. package/dist/tanstack-router.mjs +8 -0
  24. package/package.json +34 -3
  25. package/dist/__tests__/languageFlags.test.d.ts +0 -1
  26. package/dist/__tests__/languageFlags.test.js +0 -42
  27. package/dist/__tests__/mergeEntitlements.test.d.ts +0 -1
  28. package/dist/__tests__/mergeEntitlements.test.js +0 -27
  29. package/dist/components/AixsterProvider.d.ts +0 -1
  30. package/dist/components/AixsterProvider.js +0 -1
  31. package/dist/components/LangLink.d.ts +0 -20
  32. package/dist/components/LangLink.js +0 -38
  33. package/dist/components/LangRouter.d.ts +0 -37
  34. package/dist/components/LangRouter.js +0 -191
  35. package/dist/components/LanguageSwitcher.d.ts +0 -17
  36. package/dist/components/LanguageSwitcher.js +0 -257
  37. package/dist/components/LovalingoProvider.d.ts +0 -10
  38. package/dist/components/LovalingoProvider.js +0 -413
  39. package/dist/components/NavigationOverlay.d.ts +0 -6
  40. package/dist/components/NavigationOverlay.js +0 -22
  41. package/dist/components/provider/__tests__/seoUtils.test.d.ts +0 -1
  42. package/dist/components/provider/__tests__/seoUtils.test.js +0 -13
  43. package/dist/components/provider/editModeUtils.d.ts +0 -6
  44. package/dist/components/provider/editModeUtils.js +0 -59
  45. package/dist/components/provider/localeUtils.d.ts +0 -8
  46. package/dist/components/provider/localeUtils.js +0 -46
  47. package/dist/components/provider/providerConstants.d.ts +0 -12
  48. package/dist/components/provider/providerConstants.js +0 -11
  49. package/dist/components/provider/seoUtils.d.ts +0 -8
  50. package/dist/components/provider/seoUtils.js +0 -118
  51. package/dist/components/provider/useEditModeOverlay.d.ts +0 -7
  52. package/dist/components/provider/useEditModeOverlay.js +0 -134
  53. package/dist/components/provider/useHistoryNavigationPatch.d.ts +0 -3
  54. package/dist/components/provider/useHistoryNavigationPatch.js +0 -47
  55. package/dist/components/provider/useProviderCache.d.ts +0 -12
  56. package/dist/components/provider/useProviderCache.js +0 -82
  57. package/dist/context/AixsterContext.d.ts +0 -3
  58. package/dist/context/AixsterContext.js +0 -2
  59. package/dist/context/LangContext.d.ts +0 -1
  60. package/dist/context/LangContext.js +0 -2
  61. package/dist/context/LangRoutingContext.d.ts +0 -8
  62. package/dist/context/LangRoutingContext.js +0 -7
  63. package/dist/context/LovalingoContext.d.ts +0 -1
  64. package/dist/context/LovalingoContext.js +0 -1
  65. package/dist/hooks/provider/useBundleLoading.d.ts +0 -33
  66. package/dist/hooks/provider/useBundleLoading.js +0 -380
  67. package/dist/hooks/provider/useDomRules.d.ts +0 -15
  68. package/dist/hooks/provider/useDomRules.js +0 -38
  69. package/dist/hooks/provider/useLinkAutoPrefix.d.ts +0 -12
  70. package/dist/hooks/provider/useLinkAutoPrefix.js +0 -146
  71. package/dist/hooks/provider/useNavigationPrefetch.d.ts +0 -12
  72. package/dist/hooks/provider/useNavigationPrefetch.js +0 -82
  73. package/dist/hooks/provider/usePageviewTracking.d.ts +0 -10
  74. package/dist/hooks/provider/usePageviewTracking.js +0 -44
  75. package/dist/hooks/provider/usePrehide.d.ts +0 -5
  76. package/dist/hooks/provider/usePrehide.js +0 -72
  77. package/dist/hooks/provider/useSitemapLinkTag.d.ts +0 -7
  78. package/dist/hooks/provider/useSitemapLinkTag.js +0 -28
  79. package/dist/hooks/provider/useStringMissReporting.d.ts +0 -14
  80. package/dist/hooks/provider/useStringMissReporting.js +0 -155
  81. package/dist/hooks/useAixster.d.ts +0 -6
  82. package/dist/hooks/useAixster.js +0 -14
  83. package/dist/hooks/useAixsterEdit.d.ts +0 -5
  84. package/dist/hooks/useAixsterEdit.js +0 -13
  85. package/dist/hooks/useAixsterTranslate.d.ts +0 -4
  86. package/dist/hooks/useAixsterTranslate.js +0 -12
  87. package/dist/hooks/useLang.d.ts +0 -16
  88. package/dist/hooks/useLang.js +0 -23
  89. package/dist/hooks/useLangNavigate.d.ts +0 -24
  90. package/dist/hooks/useLangNavigate.js +0 -40
  91. package/dist/hooks/useLovalingo.d.ts +0 -1
  92. package/dist/hooks/useLovalingo.js +0 -1
  93. package/dist/hooks/useLovalingoEdit.d.ts +0 -1
  94. package/dist/hooks/useLovalingoEdit.js +0 -1
  95. package/dist/hooks/useLovalingoTranslate.d.ts +0 -1
  96. package/dist/hooks/useLovalingoTranslate.js +0 -1
  97. package/dist/types.d.ts +0 -76
  98. package/dist/types.js +0 -1
  99. package/dist/utils/api.d.ts +0 -42
  100. package/dist/utils/api.js +0 -395
  101. package/dist/utils/apiTypes.d.ts +0 -78
  102. package/dist/utils/apiTypes.js +0 -1
  103. package/dist/utils/apiUtils.d.ts +0 -4
  104. package/dist/utils/apiUtils.js +0 -54
  105. package/dist/utils/domRules.d.ts +0 -2
  106. package/dist/utils/domRules.js +0 -150
  107. package/dist/utils/hash.d.ts +0 -9
  108. package/dist/utils/hash.js +0 -27
  109. package/dist/utils/languageFlags.d.ts +0 -7
  110. package/dist/utils/languageFlags.js +0 -90
  111. package/dist/utils/logger.d.ts +0 -3
  112. package/dist/utils/logger.js +0 -40
  113. package/dist/utils/markerEngine.d.ts +0 -12
  114. package/dist/utils/markerEngine.js +0 -109
  115. package/dist/utils/markerEngineApply.d.ts +0 -3
  116. package/dist/utils/markerEngineApply.js +0 -136
  117. package/dist/utils/markerEngineConstants.d.ts +0 -10
  118. package/dist/utils/markerEngineConstants.js +0 -12
  119. package/dist/utils/markerEngineCritical.d.ts +0 -2
  120. package/dist/utils/markerEngineCritical.js +0 -98
  121. package/dist/utils/markerEngineDomUtils.d.ts +0 -8
  122. package/dist/utils/markerEngineDomUtils.js +0 -74
  123. package/dist/utils/markerEngineFilters.d.ts +0 -2
  124. package/dist/utils/markerEngineFilters.js +0 -26
  125. package/dist/utils/markerEngineMisses.d.ts +0 -5
  126. package/dist/utils/markerEngineMisses.js +0 -81
  127. package/dist/utils/markerEngineOriginals.d.ts +0 -5
  128. package/dist/utils/markerEngineOriginals.js +0 -29
  129. package/dist/utils/markerEngineScan.d.ts +0 -5
  130. package/dist/utils/markerEngineScan.js +0 -162
  131. package/dist/utils/markerEngineState.d.ts +0 -4
  132. package/dist/utils/markerEngineState.js +0 -14
  133. package/dist/utils/markerEngineStats.d.ts +0 -3
  134. package/dist/utils/markerEngineStats.js +0 -28
  135. package/dist/utils/markerEngineTranslations.d.ts +0 -3
  136. package/dist/utils/markerEngineTranslations.js +0 -49
  137. package/dist/utils/markerEngineTypes.d.ts +0 -62
  138. package/dist/utils/markerEngineTypes.js +0 -1
  139. package/dist/utils/markerEngineViewport.d.ts +0 -2
  140. package/dist/utils/markerEngineViewport.js +0 -27
  141. package/dist/utils/mergeEntitlements.d.ts +0 -2
  142. package/dist/utils/mergeEntitlements.js +0 -7
  143. package/dist/utils/nonLocalizedPaths.d.ts +0 -12
  144. package/dist/utils/nonLocalizedPaths.js +0 -136
  145. package/dist/utils/pathNormalizer.d.ts +0 -49
  146. package/dist/utils/pathNormalizer.js +0 -115
  147. package/dist/version.d.ts +0 -1
  148. 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,6 +0,0 @@
1
- import React from 'react';
2
- interface NavigationOverlayProps {
3
- isVisible: boolean;
4
- }
5
- export declare const NavigationOverlay: React.FC<NavigationOverlayProps>;
6
- export {};
@@ -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,6 +0,0 @@
1
- export declare function readEditParams(): {
2
- enabled: boolean;
3
- editKey: string | null;
4
- };
5
- export declare function cssEscape(value: string): string;
6
- export declare function buildCssSelector(element: HTMLElement, maxDepth?: number): string | null;
@@ -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;