@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.
- package/README.md +36 -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
|
@@ -0,0 +1,3440 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LangRoutingContext,
|
|
3
|
+
errorDebug,
|
|
4
|
+
logDebug,
|
|
5
|
+
warnDebug
|
|
6
|
+
} from "./chunk-7D5LBV45.mjs";
|
|
7
|
+
import {
|
|
8
|
+
BRANDING_STORAGE_PREFIX,
|
|
9
|
+
DEFAULT_PATH_NORMALIZATION,
|
|
10
|
+
EDIT_HIGHLIGHT_ID,
|
|
11
|
+
EDIT_HINT_ID,
|
|
12
|
+
EDIT_KEY_PARAM,
|
|
13
|
+
EDIT_MODE_PARAM,
|
|
14
|
+
EDIT_MODE_VALUES,
|
|
15
|
+
EDIT_UI_ATTR,
|
|
16
|
+
LIVE_MISSES_QUERY_PARAM,
|
|
17
|
+
LOADING_BG_STORAGE_PREFIX,
|
|
18
|
+
LOCALE_STORAGE_KEY
|
|
19
|
+
} from "./chunk-ZMRCSUM7.mjs";
|
|
20
|
+
import {
|
|
21
|
+
isNonLocalizedPath,
|
|
22
|
+
stripLocalePrefix
|
|
23
|
+
} from "./chunk-2FZR2AKF.mjs";
|
|
24
|
+
|
|
25
|
+
// src/components/LovalingoProvider.tsx
|
|
26
|
+
import React3, { useCallback as useCallback7, useContext, useEffect as useEffect12, useLayoutEffect, useMemo as useMemo2, useRef as useRef9, useState as useState4 } from "react";
|
|
27
|
+
|
|
28
|
+
// src/context/AixsterContext.tsx
|
|
29
|
+
import { createContext } from "react";
|
|
30
|
+
var LovalingoContext = createContext(null);
|
|
31
|
+
|
|
32
|
+
// src/utils/apiUtils.ts
|
|
33
|
+
var OK_HTTP_STATUSES = /* @__PURE__ */ new Set([200, 201]);
|
|
34
|
+
var NOT_FOUND_TITLE_HINTS = /404|not found|page not found|page missing|does not exist|error 404/i;
|
|
35
|
+
function getNavigationResponseStatus() {
|
|
36
|
+
if (typeof performance === "undefined" || typeof performance.getEntriesByType !== "function") return null;
|
|
37
|
+
const entries = performance.getEntriesByType("navigation");
|
|
38
|
+
if (!entries || entries.length === 0) return null;
|
|
39
|
+
const entry = entries[0];
|
|
40
|
+
const rawStatus = entry?.responseStatus;
|
|
41
|
+
const status = typeof rawStatus === "number" ? Math.floor(rawStatus) : NaN;
|
|
42
|
+
if (!Number.isFinite(status) || status <= 0) return null;
|
|
43
|
+
return status;
|
|
44
|
+
}
|
|
45
|
+
function isOkHttpStatus(status) {
|
|
46
|
+
if (typeof status !== "number") return true;
|
|
47
|
+
return OK_HTTP_STATUSES.has(status);
|
|
48
|
+
}
|
|
49
|
+
function looksLikeNotFoundDocument() {
|
|
50
|
+
if (typeof document === "undefined") return false;
|
|
51
|
+
const title = (document.title || "").toString().trim().toLowerCase();
|
|
52
|
+
if (title && NOT_FOUND_TITLE_HINTS.test(title)) return true;
|
|
53
|
+
const bodyText = (document.body?.textContent || "").toString().slice(0, 2e3).toLowerCase();
|
|
54
|
+
if (!bodyText) return false;
|
|
55
|
+
const has404 = /\b404\b/.test(bodyText);
|
|
56
|
+
const hasNotFound = /not found|page not found|page missing|does not exist|error 404/.test(bodyText);
|
|
57
|
+
return has404 && hasNotFound;
|
|
58
|
+
}
|
|
59
|
+
function normalizeApiBase(raw) {
|
|
60
|
+
const input = (raw || "").toString().trim();
|
|
61
|
+
if (!input) return "https://cdn.lovalingo.com";
|
|
62
|
+
let base = input.replace(/\/functions\/v1\/?$/i, "").replace(/\/$/, "");
|
|
63
|
+
try {
|
|
64
|
+
const parsed = new URL(base);
|
|
65
|
+
const host = parsed.hostname.toLowerCase();
|
|
66
|
+
if (host.endsWith(".supabase.co")) {
|
|
67
|
+
warnDebug("LovalingoAPI", `apiBase points to Supabase (${host}); falling back to https://cdn.lovalingo.com`);
|
|
68
|
+
return "https://cdn.lovalingo.com";
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
return base;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/utils/api.ts
|
|
76
|
+
var LovalingoAPI = class {
|
|
77
|
+
constructor(apiKey, apiBase, pathConfig, editKey) {
|
|
78
|
+
this.entitlements = null;
|
|
79
|
+
this.apiKey = apiKey;
|
|
80
|
+
this.apiBase = normalizeApiBase(apiBase);
|
|
81
|
+
this.pathConfig = pathConfig;
|
|
82
|
+
this.editKey = editKey;
|
|
83
|
+
}
|
|
84
|
+
hasApiKey() {
|
|
85
|
+
return typeof this.apiKey === "string" && this.apiKey.trim().length > 0;
|
|
86
|
+
}
|
|
87
|
+
buildPathParam(pathOrUrl) {
|
|
88
|
+
if (typeof window === "undefined") return "/";
|
|
89
|
+
const input = (pathOrUrl || "").toString().trim();
|
|
90
|
+
if (!input) return window.location.pathname + window.location.search;
|
|
91
|
+
try {
|
|
92
|
+
if (/^https?:\/\//i.test(input)) {
|
|
93
|
+
const url = new URL(input);
|
|
94
|
+
return url.pathname + url.search;
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
return input;
|
|
99
|
+
}
|
|
100
|
+
warnMissingApiKey(action) {
|
|
101
|
+
warnDebug(
|
|
102
|
+
`[Lovalingo] Missing public project key: ${action} was skipped. Pass publicAnonKey to <LovalingoProvider ...> (or set VITE_LOVALINGO_PUBLIC_ANON_KEY).`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
warnMissingEditKey(action) {
|
|
106
|
+
warnDebug(`[Lovalingo] Missing edit key: ${action} was skipped. Open the edit link from the dashboard to continue.`);
|
|
107
|
+
}
|
|
108
|
+
logActivationRequired(context, response) {
|
|
109
|
+
errorDebug(
|
|
110
|
+
`[Lovalingo] ${context} blocked (HTTP ${response.status}). This project is not activated yet. Publish a public manifest at "/.well-known/lovalingo.json" on your domain, then verify it in the Lovalingo dashboard to activate translations + SEO.`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
isActivationRequiredPayload(data) {
|
|
114
|
+
if (!data || typeof data !== "object") return false;
|
|
115
|
+
const record = data;
|
|
116
|
+
const status = record["status"];
|
|
117
|
+
const errorCode = record["error_code"];
|
|
118
|
+
return status === "activation_required" || errorCode === "PROJECT_NOT_ACTIVATED";
|
|
119
|
+
}
|
|
120
|
+
isActivationRequiredResponse(response, data) {
|
|
121
|
+
if (response.status === 403) return true;
|
|
122
|
+
if (response.headers.get("X-Lovalingo-Status") === "activation_required") return true;
|
|
123
|
+
return typeof data !== "undefined" ? this.isActivationRequiredPayload(data) : false;
|
|
124
|
+
}
|
|
125
|
+
getEntitlements() {
|
|
126
|
+
return this.entitlements;
|
|
127
|
+
}
|
|
128
|
+
async fetchEntitlements(localeHint) {
|
|
129
|
+
try {
|
|
130
|
+
if (!this.hasApiKey()) {
|
|
131
|
+
this.warnMissingApiKey("fetchEntitlements");
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const pathParam = this.buildPathParam();
|
|
135
|
+
const response = await fetch(
|
|
136
|
+
`${this.apiBase}/functions/v1/bundle?key=${this.apiKey}&locale=${encodeURIComponent(localeHint)}&path=${encodeURIComponent(pathParam)}&scoped=1`
|
|
137
|
+
);
|
|
138
|
+
if (this.isActivationRequiredResponse(response)) {
|
|
139
|
+
this.logActivationRequired("fetchEntitlements", response);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (!response.ok) return null;
|
|
143
|
+
const data = await response.json();
|
|
144
|
+
if (this.isActivationRequiredResponse(response, data)) {
|
|
145
|
+
this.logActivationRequired("fetchEntitlements", response);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
if (data?.entitlements) {
|
|
149
|
+
this.entitlements = {
|
|
150
|
+
...data.entitlements,
|
|
151
|
+
seoEnabled: typeof data?.seoEnabled === "boolean" ? data.seoEnabled : void 0
|
|
152
|
+
};
|
|
153
|
+
return this.entitlements;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async fetchSeoBundle(localeHint) {
|
|
161
|
+
try {
|
|
162
|
+
if (!this.hasApiKey()) {
|
|
163
|
+
this.warnMissingApiKey("fetchSeoBundle");
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const pathParam = this.buildPathParam();
|
|
167
|
+
const requestUrl = `${this.apiBase}/functions/v1/seo-bundle?key=${this.apiKey}&locale=${encodeURIComponent(localeHint)}&path=${encodeURIComponent(pathParam)}`;
|
|
168
|
+
const response = await fetch(requestUrl);
|
|
169
|
+
if (this.isActivationRequiredResponse(response)) {
|
|
170
|
+
this.logActivationRequired("fetchSeoBundle", response);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const resolvedResponse = response.status === 304 ? await fetch(requestUrl, { cache: "force-cache" }) : response;
|
|
174
|
+
if (resolvedResponse !== response && this.isActivationRequiredResponse(resolvedResponse)) {
|
|
175
|
+
this.logActivationRequired("fetchSeoBundle", resolvedResponse);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
if (!resolvedResponse.ok) return null;
|
|
179
|
+
const data = await resolvedResponse.json();
|
|
180
|
+
if (this.isActivationRequiredResponse(resolvedResponse, data)) {
|
|
181
|
+
this.logActivationRequired("fetchSeoBundle", resolvedResponse);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return data || null;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async trackPageview(pathOrUrl, opts) {
|
|
190
|
+
try {
|
|
191
|
+
if (!this.hasApiKey()) return;
|
|
192
|
+
const status = getNavigationResponseStatus();
|
|
193
|
+
if (!isOkHttpStatus(status)) return;
|
|
194
|
+
if (looksLikeNotFoundDocument()) return;
|
|
195
|
+
const params = new URLSearchParams();
|
|
196
|
+
params.set("key", this.apiKey);
|
|
197
|
+
params.set("path", pathOrUrl);
|
|
198
|
+
const count = opts?.critical_count;
|
|
199
|
+
const hash = (opts?.critical_hash || "").toString().trim().toLowerCase();
|
|
200
|
+
if (typeof count === "number" && Number.isFinite(count) && count > 0 && count <= 5e3 && /^[a-z0-9]{1,40}$/.test(hash)) {
|
|
201
|
+
params.set("critical_count", String(Math.floor(count)));
|
|
202
|
+
params.set("critical_hash", hash);
|
|
203
|
+
}
|
|
204
|
+
const response = await fetch(`${this.apiBase}/functions/v1/pageview?${params.toString()}`, {
|
|
205
|
+
method: "GET",
|
|
206
|
+
keepalive: true
|
|
207
|
+
});
|
|
208
|
+
if (response.status === 403) {
|
|
209
|
+
this.logActivationRequired("trackPageview", response);
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async reportStringMisses(targetLocale, misses, opts) {
|
|
215
|
+
try {
|
|
216
|
+
if (!this.hasApiKey()) return null;
|
|
217
|
+
if (!Array.isArray(misses) || misses.length === 0) return null;
|
|
218
|
+
const status = getNavigationResponseStatus();
|
|
219
|
+
if (!isOkHttpStatus(status)) {
|
|
220
|
+
return { ignored: true, reason: "http_status" };
|
|
221
|
+
}
|
|
222
|
+
if (looksLikeNotFoundDocument()) {
|
|
223
|
+
return { ignored: true, reason: "soft_404" };
|
|
224
|
+
}
|
|
225
|
+
const pathParam = this.buildPathParam(opts?.pathOrUrl);
|
|
226
|
+
const response = await fetch(`${this.apiBase}/functions/v1/misses`, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { "Content-Type": "application/json" },
|
|
229
|
+
body: JSON.stringify({
|
|
230
|
+
key: this.apiKey,
|
|
231
|
+
locale: targetLocale,
|
|
232
|
+
path: pathParam,
|
|
233
|
+
source_locale: opts?.sourceLocale,
|
|
234
|
+
locales: Array.isArray(opts?.locales) ? opts?.locales : void 0,
|
|
235
|
+
page_status: typeof status === "number" ? status : void 0,
|
|
236
|
+
misses
|
|
237
|
+
})
|
|
238
|
+
});
|
|
239
|
+
if (this.isActivationRequiredResponse(response)) {
|
|
240
|
+
this.logActivationRequired("reportStringMisses", response);
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
if (!response.ok) return null;
|
|
244
|
+
const data = await response.json();
|
|
245
|
+
if (this.isActivationRequiredResponse(response, data)) {
|
|
246
|
+
this.logActivationRequired("reportStringMisses", response);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
return data;
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async fetchTranslations(sourceLocale, targetLocale) {
|
|
255
|
+
try {
|
|
256
|
+
if (!this.hasApiKey()) {
|
|
257
|
+
this.warnMissingApiKey("fetchTranslations");
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
const bundle = await this.fetchBundle(targetLocale);
|
|
261
|
+
if (!bundle) return [];
|
|
262
|
+
return Object.entries(bundle.map).map(([source_text, translated_text]) => ({
|
|
263
|
+
source_text,
|
|
264
|
+
translated_text,
|
|
265
|
+
source_locale: sourceLocale,
|
|
266
|
+
target_locale: targetLocale
|
|
267
|
+
}));
|
|
268
|
+
} catch (error) {
|
|
269
|
+
errorDebug("Error fetching translations:", error);
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async fetchBundle(localeHint, pathOrUrl) {
|
|
274
|
+
try {
|
|
275
|
+
if (!this.hasApiKey()) {
|
|
276
|
+
this.warnMissingApiKey("fetchBundle");
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const pathParam = this.buildPathParam(pathOrUrl);
|
|
280
|
+
const requestUrl = `${this.apiBase}/functions/v1/bundle?key=${this.apiKey}&locale=${encodeURIComponent(localeHint)}&path=${encodeURIComponent(pathParam)}&scoped=1`;
|
|
281
|
+
const response = await fetch(requestUrl);
|
|
282
|
+
if (this.isActivationRequiredResponse(response)) {
|
|
283
|
+
this.logActivationRequired("fetchBundle", response);
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const resolvedResponse = response.status === 304 ? await fetch(requestUrl, { cache: "force-cache" }) : response;
|
|
287
|
+
if (resolvedResponse !== response && this.isActivationRequiredResponse(resolvedResponse)) {
|
|
288
|
+
this.logActivationRequired("fetchBundle", resolvedResponse);
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
if (!resolvedResponse.ok) return null;
|
|
292
|
+
const data = await resolvedResponse.json();
|
|
293
|
+
if (this.isActivationRequiredResponse(resolvedResponse, data)) {
|
|
294
|
+
this.logActivationRequired("fetchBundle", resolvedResponse);
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
if (data?.entitlements) {
|
|
298
|
+
this.entitlements = {
|
|
299
|
+
...data.entitlements,
|
|
300
|
+
seoEnabled: typeof data?.seoEnabled === "boolean" ? data.seoEnabled : void 0
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const map = data?.map && typeof data.map === "object" ? data.map : {};
|
|
304
|
+
const hashMap = data?.hashMap && typeof data.hashMap === "object" ? data.hashMap : {};
|
|
305
|
+
return { map, hashMap };
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async fetchBootstrap(localeHint, pathOrUrl) {
|
|
311
|
+
try {
|
|
312
|
+
if (!this.hasApiKey()) {
|
|
313
|
+
this.warnMissingApiKey("fetchBootstrap");
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const pathParam = this.buildPathParam(pathOrUrl);
|
|
317
|
+
const requestUrl = `${this.apiBase}/functions/v1/bootstrap?key=${this.apiKey}&locale=${encodeURIComponent(localeHint)}&path=${encodeURIComponent(pathParam)}`;
|
|
318
|
+
const response = await fetch(requestUrl);
|
|
319
|
+
if (this.isActivationRequiredResponse(response)) {
|
|
320
|
+
this.logActivationRequired("fetchBootstrap", response);
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const resolvedResponse = response.status === 304 ? await fetch(requestUrl, { cache: "force-cache" }) : response;
|
|
324
|
+
if (resolvedResponse !== response && this.isActivationRequiredResponse(resolvedResponse)) {
|
|
325
|
+
this.logActivationRequired("fetchBootstrap", resolvedResponse);
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
if (!resolvedResponse.ok) return null;
|
|
329
|
+
const data = await resolvedResponse.json();
|
|
330
|
+
if (this.isActivationRequiredResponse(resolvedResponse, data)) {
|
|
331
|
+
this.logActivationRequired("fetchBootstrap", resolvedResponse);
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
return data || null;
|
|
335
|
+
} catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async fetchExclusions() {
|
|
340
|
+
try {
|
|
341
|
+
if (!this.hasApiKey()) {
|
|
342
|
+
this.warnMissingApiKey("fetchExclusions");
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
const response = await fetch(
|
|
346
|
+
`${this.apiBase}/functions/v1/exclusions?key=${this.apiKey}`
|
|
347
|
+
);
|
|
348
|
+
if (response.status === 403) {
|
|
349
|
+
this.logActivationRequired("fetchExclusions", response);
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
if (!response.ok) throw new Error("Failed to fetch exclusions");
|
|
353
|
+
const data = await response.json();
|
|
354
|
+
const rows = Array.isArray(data.exclusions) ? data.exclusions : [];
|
|
355
|
+
const out = [];
|
|
356
|
+
for (const row of rows) {
|
|
357
|
+
if (!row || typeof row !== "object") continue;
|
|
358
|
+
const record = row;
|
|
359
|
+
const selector = (typeof record.selector === "string" ? record.selector : "") || (typeof record.selector_value === "string" ? record.selector_value : "") || (typeof record.selectorValue === "string" ? record.selectorValue : "");
|
|
360
|
+
const type = (typeof record.type === "string" ? record.type : "") || (typeof record.selector_type === "string" ? record.selector_type : "") || (typeof record.selectorType === "string" ? record.selectorType : "");
|
|
361
|
+
const trimmedSelector = selector.trim();
|
|
362
|
+
const trimmedType = type.trim();
|
|
363
|
+
if (!trimmedSelector) continue;
|
|
364
|
+
if (trimmedType !== "css" && trimmedType !== "xpath") continue;
|
|
365
|
+
out.push({ selector: trimmedSelector, type: trimmedType });
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
} catch (error) {
|
|
369
|
+
errorDebug("Error fetching exclusions:", error);
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async fetchDomRules(targetLocale) {
|
|
374
|
+
try {
|
|
375
|
+
if (!this.hasApiKey()) {
|
|
376
|
+
this.warnMissingApiKey("fetchDomRules");
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
const pathParam = this.buildPathParam();
|
|
380
|
+
const response = await fetch(
|
|
381
|
+
`${this.apiBase}/functions/v1/dom-rules?key=${this.apiKey}&locale=${encodeURIComponent(targetLocale)}&path=${encodeURIComponent(pathParam)}`
|
|
382
|
+
);
|
|
383
|
+
if (this.isActivationRequiredResponse(response)) {
|
|
384
|
+
this.logActivationRequired("fetchDomRules", response);
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
if (!response.ok) return [];
|
|
388
|
+
const data = await response.json();
|
|
389
|
+
if (this.isActivationRequiredResponse(response, data)) {
|
|
390
|
+
this.logActivationRequired("fetchDomRules", response);
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
return Array.isArray(data?.rules) ? data.rules : [];
|
|
394
|
+
} catch (error) {
|
|
395
|
+
errorDebug("Error fetching DOM rules:", error);
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async saveExclusion(args) {
|
|
400
|
+
try {
|
|
401
|
+
if (!this.hasApiKey()) {
|
|
402
|
+
this.warnMissingApiKey("saveExclusion");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const editKey = (args.editKey || this.editKey || "").trim();
|
|
406
|
+
if (!editKey) {
|
|
407
|
+
this.warnMissingEditKey("saveExclusion");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const pagePath = (args.pagePath || this.buildPathParam()).toString();
|
|
411
|
+
const response = await fetch(`${this.apiBase}/functions/v1/exclusions?key=${this.apiKey}`, {
|
|
412
|
+
method: "POST",
|
|
413
|
+
headers: { "Content-Type": "application/json" },
|
|
414
|
+
body: JSON.stringify({
|
|
415
|
+
key: this.apiKey,
|
|
416
|
+
edit_key: editKey,
|
|
417
|
+
page_path: pagePath,
|
|
418
|
+
selector_type: args.type,
|
|
419
|
+
selector_value: args.selector,
|
|
420
|
+
description: args.description
|
|
421
|
+
})
|
|
422
|
+
});
|
|
423
|
+
if (response.status === 403) {
|
|
424
|
+
this.logActivationRequired("saveExclusion", response);
|
|
425
|
+
}
|
|
426
|
+
} catch (error) {
|
|
427
|
+
errorDebug("Error saving exclusion:", error);
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// src/utils/markerEngineConstants.ts
|
|
434
|
+
var DEFAULT_THROTTLE_MS = 150;
|
|
435
|
+
var DEFAULT_CRITICAL_BUFFER_PX = 200;
|
|
436
|
+
var DEFAULT_CRITICAL_MAX = 800;
|
|
437
|
+
var EXCLUDE_SELECTOR = "[data-lovalingo-exclude],[data-notranslate],[translate-no],[data-no-translate]";
|
|
438
|
+
var UNSAFE_CONTAINER_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template", "svg", "canvas"]);
|
|
439
|
+
var ATTRIBUTE_MARKS = [
|
|
440
|
+
{ attr: "title", marker: "data-lovalingo-title-original" },
|
|
441
|
+
{ attr: "aria-label", marker: "data-lovalingo-aria-label-original" },
|
|
442
|
+
{ attr: "placeholder", marker: "data-lovalingo-placeholder-original" }
|
|
443
|
+
];
|
|
444
|
+
var unsafeSelector = Array.from(UNSAFE_CONTAINER_TAGS).join(",");
|
|
445
|
+
|
|
446
|
+
// src/utils/markerEngineState.ts
|
|
447
|
+
var customExcludeSelector = null;
|
|
448
|
+
var activeTranslationMap = null;
|
|
449
|
+
function getCustomExcludeSelector() {
|
|
450
|
+
return customExcludeSelector;
|
|
451
|
+
}
|
|
452
|
+
function setCustomExcludeSelector(value) {
|
|
453
|
+
customExcludeSelector = value;
|
|
454
|
+
}
|
|
455
|
+
function getActiveTranslationMap() {
|
|
456
|
+
return activeTranslationMap;
|
|
457
|
+
}
|
|
458
|
+
function setActiveTranslationMap(value) {
|
|
459
|
+
activeTranslationMap = value;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/utils/markerEngineFilters.ts
|
|
463
|
+
function isExcludedElement(el) {
|
|
464
|
+
if (!el) return false;
|
|
465
|
+
if (el.closest(EXCLUDE_SELECTOR)) return true;
|
|
466
|
+
const customExcludeSelector2 = getCustomExcludeSelector();
|
|
467
|
+
if (customExcludeSelector2) {
|
|
468
|
+
try {
|
|
469
|
+
if (el.closest(customExcludeSelector2)) return true;
|
|
470
|
+
} catch {
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
function findUnsafeContainer(el) {
|
|
476
|
+
if (!el) return null;
|
|
477
|
+
if (!unsafeSelector) return null;
|
|
478
|
+
return el.closest(unsafeSelector);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/utils/hash.ts
|
|
482
|
+
function hashContent(text) {
|
|
483
|
+
if (!text || text.length === 0) {
|
|
484
|
+
return "0";
|
|
485
|
+
}
|
|
486
|
+
let hash = 5381;
|
|
487
|
+
for (let i = 0; i < text.length; i++) {
|
|
488
|
+
hash = (hash << 5) + hash + text.charCodeAt(i);
|
|
489
|
+
}
|
|
490
|
+
return Math.abs(hash).toString(36);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/utils/markerEngineDomUtils.ts
|
|
494
|
+
function getStableKey(el) {
|
|
495
|
+
const owner = el.closest("[data-lovalingo-key]");
|
|
496
|
+
const key = owner?.getAttribute("data-lovalingo-key") || "";
|
|
497
|
+
return key.trim();
|
|
498
|
+
}
|
|
499
|
+
function getElementIndex(el) {
|
|
500
|
+
const parent = el.parentElement;
|
|
501
|
+
if (!parent) return 0;
|
|
502
|
+
const children = Array.from(parent.children);
|
|
503
|
+
const idx = children.indexOf(el);
|
|
504
|
+
return idx >= 0 ? idx : 0;
|
|
505
|
+
}
|
|
506
|
+
function getTextNodeIndex(node) {
|
|
507
|
+
let index = 0;
|
|
508
|
+
let prev = node.previousSibling;
|
|
509
|
+
while (prev) {
|
|
510
|
+
if (prev.nodeType === Node.TEXT_NODE) index += 1;
|
|
511
|
+
prev = prev.previousSibling;
|
|
512
|
+
}
|
|
513
|
+
return index;
|
|
514
|
+
}
|
|
515
|
+
function buildElementPath(el) {
|
|
516
|
+
const parts = [];
|
|
517
|
+
let current = el;
|
|
518
|
+
while (current && current.tagName && current !== document.body) {
|
|
519
|
+
const tag = current.tagName.toLowerCase();
|
|
520
|
+
const idx = getElementIndex(current);
|
|
521
|
+
parts.push(`${tag}[${idx}]`);
|
|
522
|
+
current = current.parentElement;
|
|
523
|
+
}
|
|
524
|
+
parts.push("body");
|
|
525
|
+
return parts.reverse().join("/");
|
|
526
|
+
}
|
|
527
|
+
function normalizeWhitespace(value) {
|
|
528
|
+
return (value || "").toString().replace(/\s+/g, " ").trim();
|
|
529
|
+
}
|
|
530
|
+
function isTranslatableText(text) {
|
|
531
|
+
if (!text || text.trim().length < 2) return false;
|
|
532
|
+
if (/^(__[A-Z0-9_]+__\s*)+$/.test(text)) return false;
|
|
533
|
+
if (/^\d+(\.\d+)?$/.test(text)) return false;
|
|
534
|
+
if (!/[a-zA-Z\u00C0-\u024F\u1E00-\u1EFF]/.test(text)) return false;
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
function buildStableId(el, text, textIndex) {
|
|
538
|
+
const key = getStableKey(el);
|
|
539
|
+
const path = buildElementPath(el);
|
|
540
|
+
const raw = `${path}#text[${textIndex}]|${text.trim()}|${key}`;
|
|
541
|
+
return hashContent(raw);
|
|
542
|
+
}
|
|
543
|
+
function buildSelector(el) {
|
|
544
|
+
const id = el.id;
|
|
545
|
+
if (id) return `#${id.replace(/[^a-zA-Z0-9_-]/g, "\\$&")}`;
|
|
546
|
+
const className = el.className;
|
|
547
|
+
if (typeof className === "string" && className.trim()) {
|
|
548
|
+
const classes = className.split(/\s+/).map((c) => c.trim()).filter(Boolean).slice(0, 3).map((c) => `.${c.replace(/[^a-zA-Z0-9_-]/g, "\\$&")}`).join("");
|
|
549
|
+
if (classes) return classes;
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/utils/markerEngineOriginals.ts
|
|
555
|
+
var originalTextByNode = /* @__PURE__ */ new WeakMap();
|
|
556
|
+
var originalAttrByEl = /* @__PURE__ */ new WeakMap();
|
|
557
|
+
function getOrInitTextOriginal(node, parent) {
|
|
558
|
+
const existing = originalTextByNode.get(node);
|
|
559
|
+
if (existing) return existing;
|
|
560
|
+
const raw = node.nodeValue || "";
|
|
561
|
+
const leading = raw.match(/^\s*/)?.[0] ?? "";
|
|
562
|
+
const trailing = raw.match(/\s*$/)?.[0] ?? "";
|
|
563
|
+
const trimmed = raw.trim();
|
|
564
|
+
const id = buildStableId(parent, trimmed, getTextNodeIndex(node));
|
|
565
|
+
const created = { raw, trimmed, leading, trailing, id };
|
|
566
|
+
originalTextByNode.set(node, created);
|
|
567
|
+
return created;
|
|
568
|
+
}
|
|
569
|
+
function getOrInitAttrOriginal(el, attr) {
|
|
570
|
+
let map = originalAttrByEl.get(el);
|
|
571
|
+
if (!map) {
|
|
572
|
+
map = /* @__PURE__ */ new Map();
|
|
573
|
+
originalAttrByEl.set(el, map);
|
|
574
|
+
}
|
|
575
|
+
const existing = map.get(attr);
|
|
576
|
+
if (existing != null) return existing;
|
|
577
|
+
const value = (el.getAttribute(attr) || "").toString();
|
|
578
|
+
map.set(attr, value);
|
|
579
|
+
return value;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/utils/markerEngineApply.ts
|
|
583
|
+
function applyTranslationMap(bundle, root) {
|
|
584
|
+
if (!root) return 0;
|
|
585
|
+
const map = /* @__PURE__ */ new Map();
|
|
586
|
+
for (const [k, v] of Object.entries(bundle || {})) {
|
|
587
|
+
const source = normalizeWhitespace((k || "").toString());
|
|
588
|
+
const translated = (v ?? "").toString();
|
|
589
|
+
if (!source || !translated) continue;
|
|
590
|
+
map.set(source, translated);
|
|
591
|
+
}
|
|
592
|
+
setActiveTranslationMap(map);
|
|
593
|
+
return applyActiveTranslations(root);
|
|
594
|
+
}
|
|
595
|
+
function applyActiveTranslations(root = document.body) {
|
|
596
|
+
const map = getActiveTranslationMap();
|
|
597
|
+
if (!root || !map || map.size === 0) return 0;
|
|
598
|
+
let applied = 0;
|
|
599
|
+
const walk = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
600
|
+
const nodes = [];
|
|
601
|
+
let node = walk.nextNode();
|
|
602
|
+
while (node) {
|
|
603
|
+
if (node.nodeType === Node.TEXT_NODE) nodes.push(node);
|
|
604
|
+
node = walk.nextNode();
|
|
605
|
+
}
|
|
606
|
+
for (const textNode of nodes) {
|
|
607
|
+
const parent = textNode.parentElement;
|
|
608
|
+
if (!parent) continue;
|
|
609
|
+
const raw = textNode.nodeValue || "";
|
|
610
|
+
const trimmed = raw.trim();
|
|
611
|
+
if (!trimmed) continue;
|
|
612
|
+
if (isExcludedElement(parent)) continue;
|
|
613
|
+
if (findUnsafeContainer(parent)) continue;
|
|
614
|
+
if (!isTranslatableText(trimmed)) continue;
|
|
615
|
+
const original = getOrInitTextOriginal(textNode, parent);
|
|
616
|
+
const key = normalizeWhitespace(original.trimmed);
|
|
617
|
+
const translation = map.get(key);
|
|
618
|
+
if (!translation) continue;
|
|
619
|
+
const next = `${original.leading}${translation}${original.trailing}`;
|
|
620
|
+
if (textNode.nodeValue === next) continue;
|
|
621
|
+
try {
|
|
622
|
+
textNode.nodeValue = next;
|
|
623
|
+
applied += 1;
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (root instanceof HTMLElement) {
|
|
628
|
+
const elements = root.querySelectorAll("[title],[aria-label],[placeholder]");
|
|
629
|
+
elements.forEach((el) => {
|
|
630
|
+
if (isExcludedElement(el)) return;
|
|
631
|
+
if (findUnsafeContainer(el)) return;
|
|
632
|
+
for (const { attr } of ATTRIBUTE_MARKS) {
|
|
633
|
+
const current = el.getAttribute(attr);
|
|
634
|
+
if (!current) continue;
|
|
635
|
+
const trimmed = current.trim();
|
|
636
|
+
if (!trimmed || !isTranslatableText(trimmed)) continue;
|
|
637
|
+
const original = normalizeWhitespace(getOrInitAttrOriginal(el, attr));
|
|
638
|
+
const translation = map.get(original);
|
|
639
|
+
if (!translation) continue;
|
|
640
|
+
if (el.getAttribute(attr) === translation) continue;
|
|
641
|
+
try {
|
|
642
|
+
el.setAttribute(attr, translation);
|
|
643
|
+
applied += 1;
|
|
644
|
+
} catch {
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
return applied;
|
|
650
|
+
}
|
|
651
|
+
function restoreDom(root = document.body) {
|
|
652
|
+
if (!root) return;
|
|
653
|
+
const walk = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
654
|
+
let node = walk.nextNode();
|
|
655
|
+
while (node) {
|
|
656
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
657
|
+
const textNode = node;
|
|
658
|
+
const original = originalTextByNode.get(textNode);
|
|
659
|
+
if (original && textNode.nodeValue !== original.raw) {
|
|
660
|
+
try {
|
|
661
|
+
textNode.nodeValue = original.raw;
|
|
662
|
+
} catch {
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
node = walk.nextNode();
|
|
667
|
+
}
|
|
668
|
+
if (root instanceof HTMLElement) {
|
|
669
|
+
const elements = root.querySelectorAll("[title],[aria-label],[placeholder]");
|
|
670
|
+
elements.forEach((el) => {
|
|
671
|
+
const originals = originalAttrByEl.get(el);
|
|
672
|
+
if (!originals) return;
|
|
673
|
+
for (const { attr } of ATTRIBUTE_MARKS) {
|
|
674
|
+
const original = originals.get(attr);
|
|
675
|
+
if (original == null) continue;
|
|
676
|
+
if (el.getAttribute(attr) === original) continue;
|
|
677
|
+
try {
|
|
678
|
+
el.setAttribute(attr, original);
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/utils/markerEngineViewport.ts
|
|
687
|
+
function isInViewport(rect, viewportHeight, bufferPx) {
|
|
688
|
+
if (!rect) return false;
|
|
689
|
+
if (!Number.isFinite(rect.top) || !Number.isFinite(rect.bottom)) return false;
|
|
690
|
+
if (rect.width <= 0 || rect.height <= 0) return false;
|
|
691
|
+
return rect.bottom > -bufferPx && rect.top < viewportHeight + bufferPx;
|
|
692
|
+
}
|
|
693
|
+
function getTextNodeRect(node) {
|
|
694
|
+
try {
|
|
695
|
+
const range = document.createRange();
|
|
696
|
+
range.selectNodeContents(node);
|
|
697
|
+
const rect = range.getBoundingClientRect();
|
|
698
|
+
if (rect && rect.width > 0 && rect.height > 0) return rect;
|
|
699
|
+
} catch {
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
return node.parentElement ? node.parentElement.getBoundingClientRect() : null;
|
|
703
|
+
} catch {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/utils/markerEngineCritical.ts
|
|
709
|
+
function scanCriticalTexts() {
|
|
710
|
+
const root = document.body;
|
|
711
|
+
const viewportHeight = Math.max(0, Math.floor(window.innerHeight || 0));
|
|
712
|
+
const viewportWidth = Math.max(0, Math.floor(window.innerWidth || 0));
|
|
713
|
+
const viewport = { width: viewportWidth, height: viewportHeight };
|
|
714
|
+
if (!root || viewportHeight <= 0) return { texts: [], viewport };
|
|
715
|
+
const seen = /* @__PURE__ */ new Set();
|
|
716
|
+
const texts = [];
|
|
717
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
718
|
+
let node = walker.nextNode();
|
|
719
|
+
while (node && texts.length < DEFAULT_CRITICAL_MAX) {
|
|
720
|
+
if (node.nodeType !== Node.TEXT_NODE) {
|
|
721
|
+
node = walker.nextNode();
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const textNode = node;
|
|
725
|
+
const raw = textNode.nodeValue || "";
|
|
726
|
+
const trimmed = raw.trim();
|
|
727
|
+
if (!trimmed || !isTranslatableText(trimmed)) {
|
|
728
|
+
node = walker.nextNode();
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
const parent = textNode.parentElement;
|
|
732
|
+
if (!parent || isExcludedElement(parent) || findUnsafeContainer(parent)) {
|
|
733
|
+
node = walker.nextNode();
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
const original = getOrInitTextOriginal(textNode, parent);
|
|
737
|
+
const originalText = normalizeWhitespace(original.trimmed);
|
|
738
|
+
if (!originalText || seen.has(originalText)) {
|
|
739
|
+
node = walker.nextNode();
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const rect = getTextNodeRect(textNode);
|
|
743
|
+
if (!isInViewport(rect, viewportHeight, DEFAULT_CRITICAL_BUFFER_PX)) {
|
|
744
|
+
node = walker.nextNode();
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
seen.add(originalText);
|
|
748
|
+
texts.push(originalText);
|
|
749
|
+
node = walker.nextNode();
|
|
750
|
+
}
|
|
751
|
+
if (texts.length < DEFAULT_CRITICAL_MAX) {
|
|
752
|
+
const nodes = root.querySelectorAll("[title],[aria-label],[placeholder]");
|
|
753
|
+
nodes.forEach((el) => {
|
|
754
|
+
if (texts.length >= DEFAULT_CRITICAL_MAX) return;
|
|
755
|
+
if (isExcludedElement(el) || findUnsafeContainer(el)) return;
|
|
756
|
+
let rect = null;
|
|
757
|
+
try {
|
|
758
|
+
rect = el.getBoundingClientRect();
|
|
759
|
+
} catch {
|
|
760
|
+
rect = null;
|
|
761
|
+
}
|
|
762
|
+
if (!isInViewport(rect, viewportHeight, DEFAULT_CRITICAL_BUFFER_PX)) return;
|
|
763
|
+
for (const { attr } of ATTRIBUTE_MARKS) {
|
|
764
|
+
if (texts.length >= DEFAULT_CRITICAL_MAX) break;
|
|
765
|
+
const value = el.getAttribute(attr);
|
|
766
|
+
if (!value) continue;
|
|
767
|
+
const trimmed = value.trim();
|
|
768
|
+
if (!trimmed || !isTranslatableText(trimmed)) continue;
|
|
769
|
+
const original = normalizeWhitespace(getOrInitAttrOriginal(el, attr));
|
|
770
|
+
if (!original || seen.has(original)) continue;
|
|
771
|
+
seen.add(original);
|
|
772
|
+
texts.push(original);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
return { texts, viewport };
|
|
777
|
+
}
|
|
778
|
+
function getCriticalFingerprint() {
|
|
779
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
780
|
+
return { critical_count: 0, critical_hash: "0", viewport: { width: 0, height: 0 } };
|
|
781
|
+
}
|
|
782
|
+
const { texts, viewport } = scanCriticalTexts();
|
|
783
|
+
const normalized = texts.map((t) => normalizeWhitespace(t)).filter(Boolean);
|
|
784
|
+
normalized.sort((a, b) => a.localeCompare(b));
|
|
785
|
+
return {
|
|
786
|
+
critical_count: normalized.length,
|
|
787
|
+
critical_hash: hashContent(normalized.join("\n")),
|
|
788
|
+
viewport
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// src/utils/markerEngineStats.ts
|
|
793
|
+
function buildEmptyStats() {
|
|
794
|
+
return {
|
|
795
|
+
totalTextNodes: 0,
|
|
796
|
+
markedNodes: 0,
|
|
797
|
+
skippedUnsafeNodes: 0,
|
|
798
|
+
skippedExcludedNodes: 0,
|
|
799
|
+
skippedNonTranslatableNodes: 0,
|
|
800
|
+
totalChars: 0,
|
|
801
|
+
markedChars: 0,
|
|
802
|
+
skippedUnsafeChars: 0,
|
|
803
|
+
skippedExcludedChars: 0,
|
|
804
|
+
skippedNonTranslatableChars: 0,
|
|
805
|
+
coverageRatio: 0,
|
|
806
|
+
coverageRatioChars: 0
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function finalizeStats(stats) {
|
|
810
|
+
const eligibleNodes = stats.totalTextNodes - stats.skippedUnsafeNodes - stats.skippedExcludedNodes - stats.skippedNonTranslatableNodes;
|
|
811
|
+
const eligibleChars = stats.totalChars - stats.skippedUnsafeChars - stats.skippedExcludedChars - stats.skippedNonTranslatableChars;
|
|
812
|
+
stats.coverageRatio = eligibleNodes > 0 ? stats.markedNodes / eligibleNodes : 1;
|
|
813
|
+
stats.coverageRatioChars = eligibleChars > 0 ? stats.markedChars / eligibleChars : 1;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/utils/markerEngineScan.ts
|
|
817
|
+
function considerTextNode(node, stats, segments, occurrences, seen, maxSegments, critical) {
|
|
818
|
+
const raw = node.nodeValue || "";
|
|
819
|
+
if (!raw) return;
|
|
820
|
+
const trimmed = raw.trim();
|
|
821
|
+
if (!trimmed) return;
|
|
822
|
+
stats.totalTextNodes += 1;
|
|
823
|
+
stats.totalChars += raw.length;
|
|
824
|
+
const parent = node.parentElement;
|
|
825
|
+
if (!parent) return;
|
|
826
|
+
if (isExcludedElement(parent)) {
|
|
827
|
+
stats.skippedExcludedNodes += 1;
|
|
828
|
+
stats.skippedExcludedChars += raw.length;
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
const unsafe = findUnsafeContainer(parent);
|
|
832
|
+
if (unsafe) {
|
|
833
|
+
stats.skippedUnsafeNodes += 1;
|
|
834
|
+
stats.skippedUnsafeChars += raw.length;
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (!isTranslatableText(trimmed)) {
|
|
838
|
+
stats.skippedNonTranslatableNodes += 1;
|
|
839
|
+
stats.skippedNonTranslatableChars += raw.length;
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const original = getOrInitTextOriginal(node, parent);
|
|
843
|
+
stats.markedNodes += 1;
|
|
844
|
+
stats.markedChars += raw.length;
|
|
845
|
+
if (segments.length < maxSegments) {
|
|
846
|
+
const originalText = normalizeWhitespace(original.trimmed) || null;
|
|
847
|
+
const currentText = normalizeWhitespace(node.nodeValue || "") || null;
|
|
848
|
+
segments.push({
|
|
849
|
+
kind: "text",
|
|
850
|
+
selector: buildSelector(parent),
|
|
851
|
+
original: originalText,
|
|
852
|
+
current: currentText,
|
|
853
|
+
html: null
|
|
854
|
+
});
|
|
855
|
+
if (originalText && !seen.has(originalText)) {
|
|
856
|
+
seen.add(originalText);
|
|
857
|
+
occurrences.push({ source_text: originalText, semantic_context: "text" });
|
|
858
|
+
}
|
|
859
|
+
if (critical?.enabled && originalText && !critical.seen.has(originalText)) {
|
|
860
|
+
const rect = getTextNodeRect(node);
|
|
861
|
+
if (isInViewport(rect, critical.viewportHeight, critical.bufferPx)) {
|
|
862
|
+
critical.seen.add(originalText);
|
|
863
|
+
critical.occurrences.push({ source_text: originalText, semantic_context: "critical:text" });
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function considerAttributes(root, segments, occurrences, seen, maxSegments, critical) {
|
|
869
|
+
const nodes = root.querySelectorAll("[title],[aria-label],[placeholder]");
|
|
870
|
+
nodes.forEach((el) => {
|
|
871
|
+
if (isExcludedElement(el)) return;
|
|
872
|
+
if (findUnsafeContainer(el)) return;
|
|
873
|
+
for (const { attr } of ATTRIBUTE_MARKS) {
|
|
874
|
+
const value = el.getAttribute(attr);
|
|
875
|
+
if (!value) continue;
|
|
876
|
+
const trimmed = value.trim();
|
|
877
|
+
if (!trimmed || !isTranslatableText(trimmed)) continue;
|
|
878
|
+
const original = normalizeWhitespace(getOrInitAttrOriginal(el, attr)) || null;
|
|
879
|
+
const current = normalizeWhitespace(el.getAttribute(attr) || "") || null;
|
|
880
|
+
const kind = attr === "title" ? "title" : attr === "aria-label" ? "aria-label" : "placeholder";
|
|
881
|
+
if (segments.length < maxSegments) {
|
|
882
|
+
segments.push({
|
|
883
|
+
kind,
|
|
884
|
+
selector: buildSelector(el),
|
|
885
|
+
original,
|
|
886
|
+
current,
|
|
887
|
+
html: null
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
if (original && !seen.has(original)) {
|
|
891
|
+
seen.add(original);
|
|
892
|
+
occurrences.push({ source_text: original, semantic_context: `attr:${attr}` });
|
|
893
|
+
}
|
|
894
|
+
if (critical?.enabled && original && !critical.seen.has(original)) {
|
|
895
|
+
let rect = null;
|
|
896
|
+
try {
|
|
897
|
+
rect = el.getBoundingClientRect();
|
|
898
|
+
} catch {
|
|
899
|
+
rect = null;
|
|
900
|
+
}
|
|
901
|
+
if (isInViewport(rect, critical.viewportHeight, critical.bufferPx)) {
|
|
902
|
+
critical.seen.add(original);
|
|
903
|
+
critical.occurrences.push({ source_text: original, semantic_context: `critical:attr:${attr}` });
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
function scanDom(opts) {
|
|
910
|
+
const root = document.body;
|
|
911
|
+
if (!root) {
|
|
912
|
+
const empty = buildEmptyStats();
|
|
913
|
+
return { version: 1, stats: empty, segments: [], occurrences: [], truncated: false };
|
|
914
|
+
}
|
|
915
|
+
const stats = buildEmptyStats();
|
|
916
|
+
const maxSegments = Math.max(0, Math.floor(opts.maxSegments || 0)) || 2e4;
|
|
917
|
+
const includeCritical = opts.includeCritical === true;
|
|
918
|
+
const viewportHeight = includeCritical ? Math.max(0, Math.floor(window.innerHeight || 0)) : 0;
|
|
919
|
+
const viewportWidth = includeCritical ? Math.max(0, Math.floor(window.innerWidth || 0)) : 0;
|
|
920
|
+
const critical = includeCritical ? {
|
|
921
|
+
enabled: true,
|
|
922
|
+
viewportHeight,
|
|
923
|
+
bufferPx: DEFAULT_CRITICAL_BUFFER_PX,
|
|
924
|
+
max: DEFAULT_CRITICAL_MAX,
|
|
925
|
+
seen: /* @__PURE__ */ new Set(),
|
|
926
|
+
occurrences: []
|
|
927
|
+
} : null;
|
|
928
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
929
|
+
const nodes = [];
|
|
930
|
+
const segments = [];
|
|
931
|
+
const occurrences = [];
|
|
932
|
+
const seen = /* @__PURE__ */ new Set();
|
|
933
|
+
let node = walker.nextNode();
|
|
934
|
+
while (node) {
|
|
935
|
+
if (node.nodeType === Node.TEXT_NODE) nodes.push(node);
|
|
936
|
+
node = walker.nextNode();
|
|
937
|
+
}
|
|
938
|
+
nodes.forEach((textNode) => {
|
|
939
|
+
if (critical?.enabled && critical.occurrences.length >= critical.max) {
|
|
940
|
+
critical.enabled = false;
|
|
941
|
+
}
|
|
942
|
+
considerTextNode(textNode, stats, segments, occurrences, seen, maxSegments, critical);
|
|
943
|
+
});
|
|
944
|
+
considerAttributes(root, segments, occurrences, seen, maxSegments, critical);
|
|
945
|
+
finalizeStats(stats);
|
|
946
|
+
const truncated = segments.length >= maxSegments;
|
|
947
|
+
return {
|
|
948
|
+
version: 1,
|
|
949
|
+
stats,
|
|
950
|
+
segments,
|
|
951
|
+
occurrences,
|
|
952
|
+
...includeCritical ? {
|
|
953
|
+
critical_occurrences: critical?.occurrences ?? [],
|
|
954
|
+
viewport: { width: viewportWidth, height: viewportHeight }
|
|
955
|
+
} : {},
|
|
956
|
+
truncated
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/utils/markerEngineMisses.ts
|
|
961
|
+
function scanDomForMisses(opts) {
|
|
962
|
+
const root = document.body;
|
|
963
|
+
const misses = [];
|
|
964
|
+
if (!root) {
|
|
965
|
+
return { misses };
|
|
966
|
+
}
|
|
967
|
+
const translationMap = getActiveTranslationMap();
|
|
968
|
+
const hasTranslations = Boolean(translationMap && translationMap.size > 0);
|
|
969
|
+
const max = Math.max(0, Math.floor(opts.max || 0));
|
|
970
|
+
if (max <= 0) return { misses };
|
|
971
|
+
const ignore = opts.ignore || /* @__PURE__ */ new Set();
|
|
972
|
+
const seen = /* @__PURE__ */ new Set();
|
|
973
|
+
const recordMiss = (text, context) => {
|
|
974
|
+
if (!text || seen.has(text) || ignore.has(text)) return;
|
|
975
|
+
if (hasTranslations && translationMap.has(text)) return;
|
|
976
|
+
if (misses.length >= max) return;
|
|
977
|
+
seen.add(text);
|
|
978
|
+
misses.push({ source_text: text, semantic_context: context });
|
|
979
|
+
};
|
|
980
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
981
|
+
let node = walker.nextNode();
|
|
982
|
+
while (node && misses.length < max) {
|
|
983
|
+
if (node.nodeType !== Node.TEXT_NODE) {
|
|
984
|
+
node = walker.nextNode();
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
const textNode = node;
|
|
988
|
+
const parent = textNode.parentElement;
|
|
989
|
+
if (!parent || isExcludedElement(parent) || findUnsafeContainer(parent)) {
|
|
990
|
+
node = walker.nextNode();
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const raw = textNode.nodeValue || "";
|
|
994
|
+
const trimmed = raw.trim();
|
|
995
|
+
if (!trimmed || !isTranslatableText(trimmed)) {
|
|
996
|
+
node = walker.nextNode();
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
const original = getOrInitTextOriginal(textNode, parent);
|
|
1000
|
+
const key = normalizeWhitespace(original.trimmed);
|
|
1001
|
+
if (key) {
|
|
1002
|
+
recordMiss(key, "text");
|
|
1003
|
+
}
|
|
1004
|
+
node = walker.nextNode();
|
|
1005
|
+
}
|
|
1006
|
+
if (misses.length < max) {
|
|
1007
|
+
const nodes = root.querySelectorAll("[title],[aria-label],[placeholder]");
|
|
1008
|
+
nodes.forEach((el) => {
|
|
1009
|
+
if (misses.length >= max) return;
|
|
1010
|
+
if (isExcludedElement(el) || findUnsafeContainer(el)) return;
|
|
1011
|
+
for (const { attr } of ATTRIBUTE_MARKS) {
|
|
1012
|
+
if (misses.length >= max) break;
|
|
1013
|
+
const value = el.getAttribute(attr);
|
|
1014
|
+
if (!value) continue;
|
|
1015
|
+
const trimmed = value.trim();
|
|
1016
|
+
if (!trimmed || !isTranslatableText(trimmed)) continue;
|
|
1017
|
+
const original = normalizeWhitespace(getOrInitAttrOriginal(el, attr));
|
|
1018
|
+
if (!original) continue;
|
|
1019
|
+
const context = attr === "title" ? "attr:title" : attr === "aria-label" ? "attr:aria-label" : "attr:placeholder";
|
|
1020
|
+
recordMiss(original, context);
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
return { misses };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// src/utils/markerEngineTranslations.ts
|
|
1028
|
+
function setActiveTranslations(translations) {
|
|
1029
|
+
if (!translations || translations.length === 0) {
|
|
1030
|
+
setActiveTranslationMap(null);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const map = /* @__PURE__ */ new Map();
|
|
1034
|
+
for (const t of translations) {
|
|
1035
|
+
const source = normalizeWhitespace((t?.source_text || "").toString());
|
|
1036
|
+
const translated = (t?.translated_text ?? "").toString();
|
|
1037
|
+
if (!source || !translated) continue;
|
|
1038
|
+
map.set(source, translated);
|
|
1039
|
+
}
|
|
1040
|
+
setActiveTranslationMap(map);
|
|
1041
|
+
}
|
|
1042
|
+
function addActiveTranslations(translations) {
|
|
1043
|
+
if (!translations) return 0;
|
|
1044
|
+
const map = getActiveTranslationMap() ?? /* @__PURE__ */ new Map();
|
|
1045
|
+
let added = 0;
|
|
1046
|
+
if (Array.isArray(translations)) {
|
|
1047
|
+
for (const t of translations) {
|
|
1048
|
+
const source = normalizeWhitespace((t?.source_text || "").toString());
|
|
1049
|
+
const translated = (t?.translated_text ?? "").toString();
|
|
1050
|
+
if (!source || !translated) continue;
|
|
1051
|
+
if (map.get(source) === translated) continue;
|
|
1052
|
+
map.set(source, translated);
|
|
1053
|
+
added += 1;
|
|
1054
|
+
}
|
|
1055
|
+
} else {
|
|
1056
|
+
for (const [keyRaw, valueRaw] of Object.entries(translations || {})) {
|
|
1057
|
+
const source = normalizeWhitespace((keyRaw || "").toString());
|
|
1058
|
+
const translated = (valueRaw ?? "").toString();
|
|
1059
|
+
if (!source || !translated) continue;
|
|
1060
|
+
if (map.get(source) === translated) continue;
|
|
1061
|
+
map.set(source, translated);
|
|
1062
|
+
added += 1;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
setActiveTranslationMap(map);
|
|
1066
|
+
return added;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/utils/markerEngine.ts
|
|
1070
|
+
var observer = null;
|
|
1071
|
+
var scheduled = null;
|
|
1072
|
+
var running = false;
|
|
1073
|
+
var lastStats = buildEmptyStats();
|
|
1074
|
+
var throttleMs = DEFAULT_THROTTLE_MS;
|
|
1075
|
+
var applying = false;
|
|
1076
|
+
function scanDomWithGlobals(opts) {
|
|
1077
|
+
const result = scanDom(opts);
|
|
1078
|
+
setGlobalStats(result.stats);
|
|
1079
|
+
return result;
|
|
1080
|
+
}
|
|
1081
|
+
function setGlobalStats(stats) {
|
|
1082
|
+
lastStats = stats;
|
|
1083
|
+
if (typeof window === "undefined") return;
|
|
1084
|
+
window.__lovalingoMarkersReady = true;
|
|
1085
|
+
window.__lovalingoMarkerStats = stats;
|
|
1086
|
+
const g = window;
|
|
1087
|
+
if (!g.__lovalingo) g.__lovalingo = {};
|
|
1088
|
+
if (!g.__lovalingo.dom) g.__lovalingo.dom = {};
|
|
1089
|
+
g.__lovalingo.dom.getStats = () => lastStats;
|
|
1090
|
+
g.__lovalingo.dom.scan = () => scanDomWithGlobals({ maxSegments: 2e4, includeCritical: true });
|
|
1091
|
+
g.__lovalingo.dom.getCriticalFingerprint = () => getCriticalFingerprint();
|
|
1092
|
+
g.__lovalingo.dom.apply = (bundle) => ({ applied: applyTranslationMap(bundle, document.body) });
|
|
1093
|
+
g.__lovalingo.dom.restore = () => restoreDom(document.body);
|
|
1094
|
+
}
|
|
1095
|
+
function scheduleScan() {
|
|
1096
|
+
if (!running) return;
|
|
1097
|
+
if (scheduled != null) return;
|
|
1098
|
+
scheduled = window.setTimeout(() => {
|
|
1099
|
+
scheduled = null;
|
|
1100
|
+
try {
|
|
1101
|
+
scanDomWithGlobals({ maxSegments: 2e4 });
|
|
1102
|
+
if (getActiveTranslationMap()) {
|
|
1103
|
+
applying = true;
|
|
1104
|
+
applyActiveTranslations(document.body);
|
|
1105
|
+
}
|
|
1106
|
+
} finally {
|
|
1107
|
+
applying = false;
|
|
1108
|
+
}
|
|
1109
|
+
}, throttleMs);
|
|
1110
|
+
}
|
|
1111
|
+
function startMarkerEngine(options = {}) {
|
|
1112
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1113
|
+
return () => void 0;
|
|
1114
|
+
}
|
|
1115
|
+
stopMarkerEngine();
|
|
1116
|
+
running = true;
|
|
1117
|
+
throttleMs = Math.max(20, options.throttleMs ?? DEFAULT_THROTTLE_MS);
|
|
1118
|
+
const startObserver = () => {
|
|
1119
|
+
if (!running) return;
|
|
1120
|
+
if (!document.body) {
|
|
1121
|
+
window.setTimeout(startObserver, 50);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
observer = new MutationObserver(() => {
|
|
1125
|
+
if (applying) return;
|
|
1126
|
+
scheduleScan();
|
|
1127
|
+
});
|
|
1128
|
+
observer.observe(document.body, {
|
|
1129
|
+
childList: true,
|
|
1130
|
+
subtree: true,
|
|
1131
|
+
characterData: true
|
|
1132
|
+
});
|
|
1133
|
+
scanDomWithGlobals({ maxSegments: 2e4 });
|
|
1134
|
+
};
|
|
1135
|
+
startObserver();
|
|
1136
|
+
return stopMarkerEngine;
|
|
1137
|
+
}
|
|
1138
|
+
function stopMarkerEngine() {
|
|
1139
|
+
running = false;
|
|
1140
|
+
if (scheduled != null) {
|
|
1141
|
+
window.clearTimeout(scheduled);
|
|
1142
|
+
scheduled = null;
|
|
1143
|
+
}
|
|
1144
|
+
if (observer) {
|
|
1145
|
+
observer.disconnect();
|
|
1146
|
+
observer = null;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function setMarkerEngineExclusions(exclusions) {
|
|
1150
|
+
if (!exclusions || exclusions.length === 0) {
|
|
1151
|
+
setCustomExcludeSelector(null);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const selectors = exclusions.filter((e) => e && e.type === "css" && typeof e.selector === "string" && e.selector.trim()).map((e) => e.selector.trim());
|
|
1155
|
+
setCustomExcludeSelector(selectors.length ? selectors.join(",") : null);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/utils/pathNormalizer.ts
|
|
1159
|
+
var UUID_PATTERN = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/gi;
|
|
1160
|
+
var NUMERIC_ID_PATTERN = /\/\d+(?=\/|$)/g;
|
|
1161
|
+
var HASH_PATTERN = /\/[a-f0-9]{32,}(?=\/|$)/gi;
|
|
1162
|
+
var ALPHANUMERIC_ID_PATTERN = /\/[a-z0-9]{20,}(?=\/|$)/gi;
|
|
1163
|
+
function normalizePath(path, config) {
|
|
1164
|
+
if (config?.enabled === false) {
|
|
1165
|
+
return path;
|
|
1166
|
+
}
|
|
1167
|
+
let normalized = path;
|
|
1168
|
+
let shouldIncludeSubpaths = false;
|
|
1169
|
+
normalized = normalized.replace(UUID_PATTERN, ":id");
|
|
1170
|
+
normalized = normalized.replace(HASH_PATTERN, ":hash");
|
|
1171
|
+
normalized = normalized.replace(ALPHANUMERIC_ID_PATTERN, ":id");
|
|
1172
|
+
normalized = normalized.replace(NUMERIC_ID_PATTERN, "/:id");
|
|
1173
|
+
if (config?.rules) {
|
|
1174
|
+
for (const rule of config.rules) {
|
|
1175
|
+
try {
|
|
1176
|
+
const regex = new RegExp(rule.pattern, "gi");
|
|
1177
|
+
const beforeReplace = normalized;
|
|
1178
|
+
normalized = normalized.replace(regex, rule.replacement);
|
|
1179
|
+
if (beforeReplace !== normalized && rule.includeSubpaths) {
|
|
1180
|
+
shouldIncludeSubpaths = true;
|
|
1181
|
+
}
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
warnDebug("[PathNormalizer] Invalid pattern:", rule.pattern, error);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
normalized = normalized.replace(/\/:id(\/):id/g, "/:id$1*");
|
|
1188
|
+
if (shouldIncludeSubpaths) {
|
|
1189
|
+
const placeholderMatch = normalized.match(/(:[a-z]+)/);
|
|
1190
|
+
if (placeholderMatch) {
|
|
1191
|
+
const placeholderIndex = normalized.indexOf(placeholderMatch[0]);
|
|
1192
|
+
const beforePlaceholder = normalized.substring(0, placeholderIndex + placeholderMatch[0].length);
|
|
1193
|
+
normalized = beforePlaceholder + "/*";
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return normalized;
|
|
1197
|
+
}
|
|
1198
|
+
function cleanPath(path, supportedLocales) {
|
|
1199
|
+
let cleaned = path;
|
|
1200
|
+
if (supportedLocales && supportedLocales.length > 0) {
|
|
1201
|
+
const segments = cleaned.split("/").filter(Boolean);
|
|
1202
|
+
if (segments.length > 0 && supportedLocales.includes(segments[0])) {
|
|
1203
|
+
cleaned = "/" + segments.slice(1).join("/");
|
|
1204
|
+
if (cleaned === "") cleaned = "/";
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (cleaned !== "/" && cleaned.endsWith("/")) {
|
|
1208
|
+
cleaned = cleaned.slice(0, -1);
|
|
1209
|
+
}
|
|
1210
|
+
return cleaned;
|
|
1211
|
+
}
|
|
1212
|
+
function processPath(path, config) {
|
|
1213
|
+
const cleaned = cleanPath(path, config?.supportedLocales);
|
|
1214
|
+
const normalized = normalizePath(cleaned, config);
|
|
1215
|
+
return normalized;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// src/hooks/provider/useBundleLoading.ts
|
|
1219
|
+
import { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef3, useState } from "react";
|
|
1220
|
+
|
|
1221
|
+
// src/utils/mergeEntitlements.ts
|
|
1222
|
+
function mergeEntitlementsSeoEnabled(entitlements, seoEnabled) {
|
|
1223
|
+
if (!entitlements) return null;
|
|
1224
|
+
if (typeof seoEnabled !== "boolean") return entitlements;
|
|
1225
|
+
return { ...entitlements, seoEnabled };
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// src/hooks/provider/useDomRules.ts
|
|
1229
|
+
import { useCallback, useRef } from "react";
|
|
1230
|
+
|
|
1231
|
+
// src/utils/domRules.ts
|
|
1232
|
+
var DEFAULT_MATCH_SELECTOR = 'button,a,label,summary,[role="button"],[role="link"],[role="tab"]';
|
|
1233
|
+
function ensureStyleTag(id, css) {
|
|
1234
|
+
const existing = document.querySelector(`style[data-lovalingo-rule="${id}"]`);
|
|
1235
|
+
if (existing) return;
|
|
1236
|
+
const style = document.createElement("style");
|
|
1237
|
+
style.setAttribute("data-lovalingo-rule", id);
|
|
1238
|
+
style.textContent = css;
|
|
1239
|
+
document.head.appendChild(style);
|
|
1240
|
+
}
|
|
1241
|
+
function ensureScriptTag(id, script) {
|
|
1242
|
+
const existing = document.querySelector(`script[data-lovalingo-rule="${id}"]`);
|
|
1243
|
+
if (existing) return;
|
|
1244
|
+
const el = document.createElement("script");
|
|
1245
|
+
el.setAttribute("data-lovalingo-rule", id);
|
|
1246
|
+
el.textContent = script;
|
|
1247
|
+
document.head.appendChild(el);
|
|
1248
|
+
}
|
|
1249
|
+
function shouldSkipElement(el, ruleId) {
|
|
1250
|
+
return el.hasAttribute(`data-lovalingo-rule-${ruleId}`);
|
|
1251
|
+
}
|
|
1252
|
+
function markElement(el, ruleId) {
|
|
1253
|
+
el.setAttribute(`data-lovalingo-rule-${ruleId}`, "1");
|
|
1254
|
+
}
|
|
1255
|
+
function collectElements(rule, matchText) {
|
|
1256
|
+
if (rule.selector) return Array.from(document.querySelectorAll(rule.selector));
|
|
1257
|
+
if (matchText) return Array.from(document.querySelectorAll(DEFAULT_MATCH_SELECTOR));
|
|
1258
|
+
return [];
|
|
1259
|
+
}
|
|
1260
|
+
function applyDomRules(rules) {
|
|
1261
|
+
if (!Array.isArray(rules) || rules.length === 0) return 0;
|
|
1262
|
+
let applied = 0;
|
|
1263
|
+
for (const rule of rules) {
|
|
1264
|
+
if (!rule || !rule.id) continue;
|
|
1265
|
+
const payload = rule.payload || {};
|
|
1266
|
+
const matchText = payload.matchText?.trim() || null;
|
|
1267
|
+
switch (rule.rule_type) {
|
|
1268
|
+
case "css": {
|
|
1269
|
+
const css = payload.css || "";
|
|
1270
|
+
if (css.trim().length > 0) {
|
|
1271
|
+
ensureStyleTag(rule.id, css);
|
|
1272
|
+
applied += 1;
|
|
1273
|
+
}
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1276
|
+
case "script": {
|
|
1277
|
+
const script = payload.script || "";
|
|
1278
|
+
if (script.trim().length > 0) {
|
|
1279
|
+
ensureScriptTag(rule.id, script);
|
|
1280
|
+
applied += 1;
|
|
1281
|
+
}
|
|
1282
|
+
break;
|
|
1283
|
+
}
|
|
1284
|
+
case "remove": {
|
|
1285
|
+
const elements = collectElements(rule, matchText);
|
|
1286
|
+
for (const el of elements) {
|
|
1287
|
+
if (shouldSkipElement(el, rule.id)) continue;
|
|
1288
|
+
el.remove();
|
|
1289
|
+
applied += 1;
|
|
1290
|
+
}
|
|
1291
|
+
break;
|
|
1292
|
+
}
|
|
1293
|
+
case "add_class": {
|
|
1294
|
+
const className = payload.className || payload.value || "";
|
|
1295
|
+
if (!className.trim()) break;
|
|
1296
|
+
const elements = collectElements(rule, matchText);
|
|
1297
|
+
for (const el of elements) {
|
|
1298
|
+
if (shouldSkipElement(el, rule.id)) continue;
|
|
1299
|
+
if (matchText) {
|
|
1300
|
+
const current = (el.textContent || "").trim();
|
|
1301
|
+
if (current !== matchText) continue;
|
|
1302
|
+
}
|
|
1303
|
+
className.split(/\s+/).forEach((cls) => cls && el.classList.add(cls));
|
|
1304
|
+
markElement(el, rule.id);
|
|
1305
|
+
applied += 1;
|
|
1306
|
+
}
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
case "set_attribute": {
|
|
1310
|
+
const attribute = payload.attribute || "";
|
|
1311
|
+
const value = payload.value || payload.text || "";
|
|
1312
|
+
if (!attribute || !value) break;
|
|
1313
|
+
const elements = collectElements(rule, matchText);
|
|
1314
|
+
for (const el of elements) {
|
|
1315
|
+
if (shouldSkipElement(el, rule.id)) continue;
|
|
1316
|
+
if (matchText) {
|
|
1317
|
+
const current = (el.textContent || "").trim();
|
|
1318
|
+
if (current !== matchText) continue;
|
|
1319
|
+
}
|
|
1320
|
+
el.setAttribute(attribute, value);
|
|
1321
|
+
markElement(el, rule.id);
|
|
1322
|
+
applied += 1;
|
|
1323
|
+
}
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
case "set_html": {
|
|
1327
|
+
const html = payload.html || "";
|
|
1328
|
+
if (!html) break;
|
|
1329
|
+
const elements = collectElements(rule, matchText);
|
|
1330
|
+
for (const el of elements) {
|
|
1331
|
+
if (shouldSkipElement(el, rule.id)) continue;
|
|
1332
|
+
if (matchText) {
|
|
1333
|
+
const current = (el.textContent || "").trim();
|
|
1334
|
+
if (current !== matchText) continue;
|
|
1335
|
+
}
|
|
1336
|
+
el.innerHTML = html;
|
|
1337
|
+
markElement(el, rule.id);
|
|
1338
|
+
applied += 1;
|
|
1339
|
+
}
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1342
|
+
case "replace_text":
|
|
1343
|
+
default: {
|
|
1344
|
+
const replacement = payload.text || payload.value || "";
|
|
1345
|
+
if (!replacement) break;
|
|
1346
|
+
const elements = collectElements(rule, matchText);
|
|
1347
|
+
for (const el of elements) {
|
|
1348
|
+
if (shouldSkipElement(el, rule.id)) continue;
|
|
1349
|
+
if (matchText) {
|
|
1350
|
+
const current = (el.textContent || "").trim();
|
|
1351
|
+
if (current !== matchText) continue;
|
|
1352
|
+
}
|
|
1353
|
+
el.textContent = replacement;
|
|
1354
|
+
markElement(el, rule.id);
|
|
1355
|
+
applied += 1;
|
|
1356
|
+
}
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
return applied;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// src/hooks/provider/useDomRules.ts
|
|
1365
|
+
function useDomRules({ apiRef, autoApplyRules }) {
|
|
1366
|
+
const domRulesCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
1367
|
+
const getCachedDomRules = useCallback((cacheKey) => {
|
|
1368
|
+
return domRulesCacheRef.current.get(cacheKey);
|
|
1369
|
+
}, []);
|
|
1370
|
+
const applyCachedDomRules = useCallback(
|
|
1371
|
+
(cacheKey, fallbackRules) => {
|
|
1372
|
+
if (!autoApplyRules) return 0;
|
|
1373
|
+
const rules = domRulesCacheRef.current.get(cacheKey) || fallbackRules || [];
|
|
1374
|
+
return applyDomRules(rules);
|
|
1375
|
+
},
|
|
1376
|
+
[autoApplyRules]
|
|
1377
|
+
);
|
|
1378
|
+
const setCachedDomRules = useCallback((cacheKey, rules) => {
|
|
1379
|
+
domRulesCacheRef.current.set(cacheKey, rules);
|
|
1380
|
+
}, []);
|
|
1381
|
+
const setAndApplyDomRules = useCallback(
|
|
1382
|
+
(cacheKey, rules) => {
|
|
1383
|
+
domRulesCacheRef.current.set(cacheKey, rules);
|
|
1384
|
+
if (!autoApplyRules) return 0;
|
|
1385
|
+
return applyDomRules(rules);
|
|
1386
|
+
},
|
|
1387
|
+
[autoApplyRules]
|
|
1388
|
+
);
|
|
1389
|
+
const fetchAndApplyDomRules = useCallback(
|
|
1390
|
+
async (cacheKey, targetLocale) => {
|
|
1391
|
+
if (!autoApplyRules) return [];
|
|
1392
|
+
const rules = await apiRef.current.fetchDomRules(targetLocale);
|
|
1393
|
+
domRulesCacheRef.current.set(cacheKey, rules);
|
|
1394
|
+
applyDomRules(rules);
|
|
1395
|
+
return rules;
|
|
1396
|
+
},
|
|
1397
|
+
[apiRef, autoApplyRules]
|
|
1398
|
+
);
|
|
1399
|
+
return {
|
|
1400
|
+
applyCachedDomRules,
|
|
1401
|
+
fetchAndApplyDomRules,
|
|
1402
|
+
getCachedDomRules,
|
|
1403
|
+
setAndApplyDomRules,
|
|
1404
|
+
setCachedDomRules
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/hooks/provider/usePrehide.ts
|
|
1409
|
+
import { useCallback as useCallback2, useEffect, useRef as useRef2 } from "react";
|
|
1410
|
+
var PREHIDE_FAILSAFE_MS = 900;
|
|
1411
|
+
function usePrehide() {
|
|
1412
|
+
const prehideStateRef = useRef2({
|
|
1413
|
+
active: false,
|
|
1414
|
+
timeoutId: null,
|
|
1415
|
+
startedAtMs: null,
|
|
1416
|
+
prevHtmlVisibility: "",
|
|
1417
|
+
prevBodyVisibility: "",
|
|
1418
|
+
prevHtmlBg: "",
|
|
1419
|
+
prevBodyBg: ""
|
|
1420
|
+
});
|
|
1421
|
+
const forceDisablePrehide = useCallback2(() => {
|
|
1422
|
+
if (typeof document === "undefined") return;
|
|
1423
|
+
const html = document.documentElement;
|
|
1424
|
+
const body = document.body;
|
|
1425
|
+
if (!html || !body) return;
|
|
1426
|
+
const state = prehideStateRef.current;
|
|
1427
|
+
if (state.timeoutId != null) {
|
|
1428
|
+
window.clearTimeout(state.timeoutId);
|
|
1429
|
+
state.timeoutId = null;
|
|
1430
|
+
}
|
|
1431
|
+
if (!state.active) return;
|
|
1432
|
+
state.active = false;
|
|
1433
|
+
state.startedAtMs = null;
|
|
1434
|
+
html.style.visibility = state.prevHtmlVisibility;
|
|
1435
|
+
body.style.visibility = state.prevBodyVisibility;
|
|
1436
|
+
html.style.backgroundColor = state.prevHtmlBg;
|
|
1437
|
+
body.style.backgroundColor = state.prevBodyBg;
|
|
1438
|
+
}, []);
|
|
1439
|
+
const enablePrehide = useCallback2(
|
|
1440
|
+
(bgColor) => {
|
|
1441
|
+
if (typeof document === "undefined") return;
|
|
1442
|
+
const html = document.documentElement;
|
|
1443
|
+
const body = document.body;
|
|
1444
|
+
if (!html || !body) return;
|
|
1445
|
+
const state = prehideStateRef.current;
|
|
1446
|
+
if (state.active && state.startedAtMs != null && Date.now() - state.startedAtMs > PREHIDE_FAILSAFE_MS * 3) {
|
|
1447
|
+
forceDisablePrehide();
|
|
1448
|
+
}
|
|
1449
|
+
if (!state.active) {
|
|
1450
|
+
state.active = true;
|
|
1451
|
+
state.startedAtMs = Date.now();
|
|
1452
|
+
state.prevHtmlVisibility = html.style.visibility || "";
|
|
1453
|
+
state.prevBodyVisibility = body.style.visibility || "";
|
|
1454
|
+
state.prevHtmlBg = html.style.backgroundColor || "";
|
|
1455
|
+
state.prevBodyBg = body.style.backgroundColor || "";
|
|
1456
|
+
}
|
|
1457
|
+
html.style.visibility = "hidden";
|
|
1458
|
+
body.style.visibility = "hidden";
|
|
1459
|
+
if (bgColor) {
|
|
1460
|
+
html.style.backgroundColor = bgColor;
|
|
1461
|
+
body.style.backgroundColor = bgColor;
|
|
1462
|
+
}
|
|
1463
|
+
if (state.timeoutId != null) {
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
state.timeoutId = window.setTimeout(() => forceDisablePrehide(), PREHIDE_FAILSAFE_MS);
|
|
1467
|
+
},
|
|
1468
|
+
[forceDisablePrehide]
|
|
1469
|
+
);
|
|
1470
|
+
const disablePrehide = forceDisablePrehide;
|
|
1471
|
+
useEffect(() => {
|
|
1472
|
+
return () => disablePrehide();
|
|
1473
|
+
}, [disablePrehide]);
|
|
1474
|
+
return { enablePrehide, disablePrehide };
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/hooks/provider/useBundleLoading.ts
|
|
1478
|
+
var CRITICAL_CACHE_PREFIX = "Lovalingo_critical_v0_3";
|
|
1479
|
+
function useBundleLoading({
|
|
1480
|
+
apiRef,
|
|
1481
|
+
resolvedApiKey,
|
|
1482
|
+
defaultLocale,
|
|
1483
|
+
routing,
|
|
1484
|
+
allLocales,
|
|
1485
|
+
nonLocalizedPaths,
|
|
1486
|
+
enhancedPathConfig,
|
|
1487
|
+
mode,
|
|
1488
|
+
autoApplyRules,
|
|
1489
|
+
seoProp,
|
|
1490
|
+
isSeoActive,
|
|
1491
|
+
applySeoBundle: applySeoBundle2,
|
|
1492
|
+
setEntitlements,
|
|
1493
|
+
setBrandingEnabled,
|
|
1494
|
+
setCachedBrandingEnabled,
|
|
1495
|
+
setCachedLoadingBgColor,
|
|
1496
|
+
getCachedLoadingBgColor
|
|
1497
|
+
}) {
|
|
1498
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
1499
|
+
const retryTimeoutRef = useRef3(null);
|
|
1500
|
+
const loadingFailsafeTimeoutRef = useRef3(null);
|
|
1501
|
+
const isNavigatingRef = useRef3(false);
|
|
1502
|
+
const inFlightLoadKeyRef = useRef3(null);
|
|
1503
|
+
const translationCacheRef = useRef3(/* @__PURE__ */ new Map());
|
|
1504
|
+
const exclusionsCacheRef = useRef3(null);
|
|
1505
|
+
const { enablePrehide, disablePrehide } = usePrehide();
|
|
1506
|
+
const { applyCachedDomRules, fetchAndApplyDomRules, getCachedDomRules, setAndApplyDomRules } = useDomRules({
|
|
1507
|
+
apiRef,
|
|
1508
|
+
autoApplyRules
|
|
1509
|
+
});
|
|
1510
|
+
const buildCriticalCacheKey = useCallback3(
|
|
1511
|
+
(targetLocale, normalizedPath) => {
|
|
1512
|
+
const key = `${resolvedApiKey || "anonymous"}:${targetLocale}:${normalizedPath || "/"}`;
|
|
1513
|
+
return `${CRITICAL_CACHE_PREFIX}:${hashContent(key)}`;
|
|
1514
|
+
},
|
|
1515
|
+
[resolvedApiKey]
|
|
1516
|
+
);
|
|
1517
|
+
const readCriticalCache = useCallback3(
|
|
1518
|
+
(targetLocale, normalizedPath) => {
|
|
1519
|
+
const key = buildCriticalCacheKey(targetLocale, normalizedPath);
|
|
1520
|
+
try {
|
|
1521
|
+
const raw = localStorage.getItem(key);
|
|
1522
|
+
if (!raw) return null;
|
|
1523
|
+
const parsed = JSON.parse(raw);
|
|
1524
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
1525
|
+
const record = parsed;
|
|
1526
|
+
const map = record.map && typeof record.map === "object" && !Array.isArray(record.map) ? record.map : null;
|
|
1527
|
+
const exclusionsRaw = Array.isArray(record.exclusions) ? record.exclusions : [];
|
|
1528
|
+
const exclusions = exclusionsRaw.map((row) => {
|
|
1529
|
+
if (!row || typeof row !== "object") return null;
|
|
1530
|
+
const r = row;
|
|
1531
|
+
const selector = typeof r.selector === "string" ? r.selector.trim() : "";
|
|
1532
|
+
const type = typeof r.type === "string" ? r.type.trim() : "";
|
|
1533
|
+
if (!selector) return null;
|
|
1534
|
+
if (type !== "css" && type !== "xpath") return null;
|
|
1535
|
+
return { selector, type };
|
|
1536
|
+
}).filter(Boolean);
|
|
1537
|
+
const bg = typeof record.loading_bg_color === "string" ? record.loading_bg_color.trim() : "";
|
|
1538
|
+
return {
|
|
1539
|
+
map: map || {},
|
|
1540
|
+
exclusions,
|
|
1541
|
+
loading_bg_color: /^#[0-9a-fA-F]{6}$/.test(bg) ? bg : null
|
|
1542
|
+
};
|
|
1543
|
+
} catch {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
},
|
|
1547
|
+
[buildCriticalCacheKey]
|
|
1548
|
+
);
|
|
1549
|
+
const writeCriticalCache = useCallback3(
|
|
1550
|
+
(targetLocale, normalizedPath, entry) => {
|
|
1551
|
+
const key = buildCriticalCacheKey(targetLocale, normalizedPath);
|
|
1552
|
+
try {
|
|
1553
|
+
localStorage.setItem(
|
|
1554
|
+
key,
|
|
1555
|
+
JSON.stringify({
|
|
1556
|
+
stored_at: Date.now(),
|
|
1557
|
+
map: entry.map || {},
|
|
1558
|
+
exclusions: entry.exclusions || [],
|
|
1559
|
+
loading_bg_color: entry.loading_bg_color
|
|
1560
|
+
})
|
|
1561
|
+
);
|
|
1562
|
+
} catch {
|
|
1563
|
+
}
|
|
1564
|
+
},
|
|
1565
|
+
[buildCriticalCacheKey]
|
|
1566
|
+
);
|
|
1567
|
+
const toTranslations = useCallback3(
|
|
1568
|
+
(map, targetLocale) => {
|
|
1569
|
+
const out = [];
|
|
1570
|
+
for (const [source_text, translated_text] of Object.entries(map || {})) {
|
|
1571
|
+
if (!source_text || !translated_text) continue;
|
|
1572
|
+
out.push({
|
|
1573
|
+
source_text,
|
|
1574
|
+
translated_text,
|
|
1575
|
+
source_locale: defaultLocale,
|
|
1576
|
+
target_locale: targetLocale
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
return out;
|
|
1580
|
+
},
|
|
1581
|
+
[defaultLocale]
|
|
1582
|
+
);
|
|
1583
|
+
const loadData = useCallback3(
|
|
1584
|
+
async (targetLocale, previousLocale) => {
|
|
1585
|
+
if (retryTimeoutRef.current) {
|
|
1586
|
+
clearTimeout(retryTimeoutRef.current);
|
|
1587
|
+
retryTimeoutRef.current = null;
|
|
1588
|
+
}
|
|
1589
|
+
if (loadingFailsafeTimeoutRef.current != null) {
|
|
1590
|
+
window.clearTimeout(loadingFailsafeTimeoutRef.current);
|
|
1591
|
+
loadingFailsafeTimeoutRef.current = null;
|
|
1592
|
+
}
|
|
1593
|
+
if (targetLocale === defaultLocale) {
|
|
1594
|
+
disablePrehide();
|
|
1595
|
+
setActiveTranslations(null);
|
|
1596
|
+
restoreDom(document.body);
|
|
1597
|
+
isNavigatingRef.current = false;
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
if (routing === "path") {
|
|
1601
|
+
const stripped = stripLocalePrefix(window.location.pathname, allLocales);
|
|
1602
|
+
if (isNonLocalizedPath(stripped, nonLocalizedPaths)) {
|
|
1603
|
+
disablePrehide();
|
|
1604
|
+
setActiveTranslations(null);
|
|
1605
|
+
restoreDom(document.body);
|
|
1606
|
+
isNavigatingRef.current = false;
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
const currentPath = window.location.pathname + window.location.search;
|
|
1611
|
+
const normalizedPath = processPath(window.location.pathname, enhancedPathConfig);
|
|
1612
|
+
const cacheKey = `${targetLocale}:${normalizedPath}`;
|
|
1613
|
+
if (inFlightLoadKeyRef.current === cacheKey) {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
inFlightLoadKeyRef.current = cacheKey;
|
|
1617
|
+
const cachedEntry = translationCacheRef.current.get(cacheKey);
|
|
1618
|
+
const cachedExclusions = exclusionsCacheRef.current;
|
|
1619
|
+
const cachedDomRules = getCachedDomRules(cacheKey);
|
|
1620
|
+
if (cachedEntry && cachedExclusions) {
|
|
1621
|
+
logDebug(`[Lovalingo] Using cached translations for ${targetLocale} on ${normalizedPath}`);
|
|
1622
|
+
enablePrehide(getCachedLoadingBgColor());
|
|
1623
|
+
setActiveTranslations(cachedEntry.translations);
|
|
1624
|
+
setMarkerEngineExclusions(cachedExclusions);
|
|
1625
|
+
if (mode === "dom") {
|
|
1626
|
+
applyActiveTranslations(document.body);
|
|
1627
|
+
}
|
|
1628
|
+
if (autoApplyRules) {
|
|
1629
|
+
applyCachedDomRules(cacheKey, cachedDomRules);
|
|
1630
|
+
void fetchAndApplyDomRules(cacheKey, targetLocale);
|
|
1631
|
+
}
|
|
1632
|
+
retryTimeoutRef.current = setTimeout(() => {
|
|
1633
|
+
if (isNavigatingRef.current) {
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
logDebug(`[Lovalingo] \u{1F504} Retry scan for late-rendering content`);
|
|
1637
|
+
if (mode === "dom") {
|
|
1638
|
+
applyActiveTranslations(document.body);
|
|
1639
|
+
}
|
|
1640
|
+
if (autoApplyRules) {
|
|
1641
|
+
applyCachedDomRules(cacheKey, cachedDomRules);
|
|
1642
|
+
}
|
|
1643
|
+
}, 500);
|
|
1644
|
+
disablePrehide();
|
|
1645
|
+
isNavigatingRef.current = false;
|
|
1646
|
+
if (inFlightLoadKeyRef.current === cacheKey) {
|
|
1647
|
+
inFlightLoadKeyRef.current = null;
|
|
1648
|
+
}
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
logDebug(`[Lovalingo] Fetching translations for ${targetLocale} on ${normalizedPath}`);
|
|
1652
|
+
setIsLoading(true);
|
|
1653
|
+
enablePrehide(getCachedLoadingBgColor());
|
|
1654
|
+
let revealedEarly = false;
|
|
1655
|
+
const revealNow = () => {
|
|
1656
|
+
if (revealedEarly) return;
|
|
1657
|
+
revealedEarly = true;
|
|
1658
|
+
disablePrehide();
|
|
1659
|
+
setIsLoading(false);
|
|
1660
|
+
if (loadingFailsafeTimeoutRef.current != null) {
|
|
1661
|
+
window.clearTimeout(loadingFailsafeTimeoutRef.current);
|
|
1662
|
+
loadingFailsafeTimeoutRef.current = null;
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
loadingFailsafeTimeoutRef.current = window.setTimeout(() => {
|
|
1666
|
+
disablePrehide();
|
|
1667
|
+
setIsLoading(false);
|
|
1668
|
+
}, PREHIDE_FAILSAFE_MS);
|
|
1669
|
+
try {
|
|
1670
|
+
if (previousLocale && previousLocale !== defaultLocale) {
|
|
1671
|
+
logDebug(`[Lovalingo] Switching from ${previousLocale} to ${targetLocale}`);
|
|
1672
|
+
}
|
|
1673
|
+
let revealedViaCachedCritical = false;
|
|
1674
|
+
const cachedCritical = readCriticalCache(targetLocale, normalizedPath);
|
|
1675
|
+
if (cachedCritical?.loading_bg_color) {
|
|
1676
|
+
setCachedLoadingBgColor(cachedCritical.loading_bg_color);
|
|
1677
|
+
enablePrehide(cachedCritical.loading_bg_color);
|
|
1678
|
+
}
|
|
1679
|
+
if (cachedCritical?.exclusions && cachedCritical.exclusions.length > 0) {
|
|
1680
|
+
exclusionsCacheRef.current = cachedCritical.exclusions;
|
|
1681
|
+
setMarkerEngineExclusions(cachedCritical.exclusions);
|
|
1682
|
+
}
|
|
1683
|
+
if (cachedCritical?.map && Object.keys(cachedCritical.map).length > 0) {
|
|
1684
|
+
setActiveTranslations(toTranslations(cachedCritical.map, targetLocale));
|
|
1685
|
+
if (mode === "dom") {
|
|
1686
|
+
applyActiveTranslations(document.body);
|
|
1687
|
+
}
|
|
1688
|
+
revealNow();
|
|
1689
|
+
revealedViaCachedCritical = true;
|
|
1690
|
+
}
|
|
1691
|
+
const bootstrap = await apiRef.current.fetchBootstrap(targetLocale, currentPath);
|
|
1692
|
+
const entitlementsBase = bootstrap?.entitlements || apiRef.current.getEntitlements();
|
|
1693
|
+
const nextEntitlements = mergeEntitlementsSeoEnabled(entitlementsBase, bootstrap?.seoEnabled);
|
|
1694
|
+
if (nextEntitlements) setEntitlements(nextEntitlements);
|
|
1695
|
+
if (bootstrap?.loading_bg_color) {
|
|
1696
|
+
setCachedLoadingBgColor(bootstrap.loading_bg_color);
|
|
1697
|
+
enablePrehide(bootstrap.loading_bg_color);
|
|
1698
|
+
}
|
|
1699
|
+
if ((bootstrap?.entitlements || nextEntitlements)?.brandingRequired) {
|
|
1700
|
+
setBrandingEnabled(true);
|
|
1701
|
+
setCachedBrandingEnabled(true);
|
|
1702
|
+
} else if (typeof bootstrap?.branding_enabled === "boolean") {
|
|
1703
|
+
setBrandingEnabled(bootstrap.branding_enabled);
|
|
1704
|
+
setCachedBrandingEnabled(bootstrap.branding_enabled);
|
|
1705
|
+
}
|
|
1706
|
+
const exclusions = Array.isArray(bootstrap?.exclusions) ? bootstrap.exclusions.map((row) => {
|
|
1707
|
+
if (!row || typeof row !== "object") return null;
|
|
1708
|
+
const r = row;
|
|
1709
|
+
const selector = typeof r.selector === "string" ? r.selector.trim() : "";
|
|
1710
|
+
const type = typeof r.type === "string" ? r.type.trim() : "";
|
|
1711
|
+
if (!selector) return null;
|
|
1712
|
+
if (type !== "css" && type !== "xpath") return null;
|
|
1713
|
+
return { selector, type };
|
|
1714
|
+
}).filter(Boolean) : await apiRef.current.fetchExclusions();
|
|
1715
|
+
exclusionsCacheRef.current = exclusions;
|
|
1716
|
+
setMarkerEngineExclusions(exclusions);
|
|
1717
|
+
const criticalMap = bootstrap?.critical?.map && typeof bootstrap.critical.map === "object" && !Array.isArray(bootstrap.critical.map) ? bootstrap.critical.map : {};
|
|
1718
|
+
const hasBootstrapCritical = Object.keys(criticalMap).length > 0;
|
|
1719
|
+
if (Object.keys(criticalMap).length > 0) {
|
|
1720
|
+
setActiveTranslations(toTranslations(criticalMap, targetLocale));
|
|
1721
|
+
if (mode === "dom") {
|
|
1722
|
+
applyActiveTranslations(document.body);
|
|
1723
|
+
}
|
|
1724
|
+
revealNow();
|
|
1725
|
+
}
|
|
1726
|
+
if (autoApplyRules) {
|
|
1727
|
+
const bootstrapDomRules = bootstrap?.dom_rules;
|
|
1728
|
+
const domRules = Array.isArray(bootstrapDomRules) ? bootstrapDomRules : await apiRef.current.fetchDomRules(targetLocale);
|
|
1729
|
+
setAndApplyDomRules(cacheKey, domRules);
|
|
1730
|
+
}
|
|
1731
|
+
let seoActiveForBootstrap = isSeoActive();
|
|
1732
|
+
if (seoProp === false) {
|
|
1733
|
+
seoActiveForBootstrap = false;
|
|
1734
|
+
} else if (typeof bootstrap?.seoEnabled === "boolean") {
|
|
1735
|
+
seoActiveForBootstrap = bootstrap.seoEnabled !== false;
|
|
1736
|
+
} else if (typeof nextEntitlements?.seoEnabled === "boolean") {
|
|
1737
|
+
seoActiveForBootstrap = nextEntitlements.seoEnabled !== false;
|
|
1738
|
+
}
|
|
1739
|
+
if (seoActiveForBootstrap && bootstrap) {
|
|
1740
|
+
const hreflangEnabled = Boolean((bootstrap.entitlements || nextEntitlements)?.hreflangEnabled);
|
|
1741
|
+
applySeoBundle2({ seo: bootstrap.seo, alternates: bootstrap.alternates, jsonld: bootstrap.jsonld }, hreflangEnabled);
|
|
1742
|
+
}
|
|
1743
|
+
writeCriticalCache(targetLocale, normalizedPath, {
|
|
1744
|
+
map: criticalMap,
|
|
1745
|
+
exclusions,
|
|
1746
|
+
loading_bg_color: bootstrap?.loading_bg_color && /^#[0-9a-fA-F]{6}$/.test(bootstrap.loading_bg_color) ? bootstrap.loading_bg_color : null
|
|
1747
|
+
});
|
|
1748
|
+
const shouldWaitForBundle = !revealedViaCachedCritical && !hasBootstrapCritical;
|
|
1749
|
+
if (shouldWaitForBundle) {
|
|
1750
|
+
const bundle = await apiRef.current.fetchBundle(targetLocale, currentPath);
|
|
1751
|
+
if (bundle?.map && typeof bundle.map === "object") {
|
|
1752
|
+
const translations = toTranslations(bundle.map, targetLocale);
|
|
1753
|
+
if (translations.length > 0) {
|
|
1754
|
+
translationCacheRef.current.set(cacheKey, { translations });
|
|
1755
|
+
setActiveTranslations(translations);
|
|
1756
|
+
if (mode === "dom") {
|
|
1757
|
+
applyActiveTranslations(document.body);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
} else {
|
|
1762
|
+
void (async () => {
|
|
1763
|
+
const bundle = await apiRef.current.fetchBundle(targetLocale, currentPath);
|
|
1764
|
+
if (!bundle || !bundle.map) return;
|
|
1765
|
+
const translations = toTranslations(bundle.map, targetLocale);
|
|
1766
|
+
if (translations.length === 0) return;
|
|
1767
|
+
translationCacheRef.current.set(cacheKey, { translations });
|
|
1768
|
+
setActiveTranslations(translations);
|
|
1769
|
+
if (mode === "dom") {
|
|
1770
|
+
applyActiveTranslations(document.body);
|
|
1771
|
+
}
|
|
1772
|
+
})();
|
|
1773
|
+
}
|
|
1774
|
+
revealNow();
|
|
1775
|
+
retryTimeoutRef.current = setTimeout(() => {
|
|
1776
|
+
if (isNavigatingRef.current) {
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
logDebug(`[Lovalingo] \u{1F504} Retry scan for late-rendering content`);
|
|
1780
|
+
if (mode === "dom") {
|
|
1781
|
+
applyActiveTranslations(document.body);
|
|
1782
|
+
}
|
|
1783
|
+
if (autoApplyRules) {
|
|
1784
|
+
applyCachedDomRules(cacheKey);
|
|
1785
|
+
}
|
|
1786
|
+
}, 500);
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
errorDebug("Error loading translations:", error);
|
|
1789
|
+
disablePrehide();
|
|
1790
|
+
} finally {
|
|
1791
|
+
setIsLoading(false);
|
|
1792
|
+
if (loadingFailsafeTimeoutRef.current != null) {
|
|
1793
|
+
window.clearTimeout(loadingFailsafeTimeoutRef.current);
|
|
1794
|
+
loadingFailsafeTimeoutRef.current = null;
|
|
1795
|
+
}
|
|
1796
|
+
isNavigatingRef.current = false;
|
|
1797
|
+
if (inFlightLoadKeyRef.current === cacheKey) {
|
|
1798
|
+
inFlightLoadKeyRef.current = null;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
},
|
|
1802
|
+
[
|
|
1803
|
+
allLocales,
|
|
1804
|
+
applyCachedDomRules,
|
|
1805
|
+
applySeoBundle2,
|
|
1806
|
+
autoApplyRules,
|
|
1807
|
+
defaultLocale,
|
|
1808
|
+
disablePrehide,
|
|
1809
|
+
enablePrehide,
|
|
1810
|
+
enhancedPathConfig,
|
|
1811
|
+
fetchAndApplyDomRules,
|
|
1812
|
+
getCachedDomRules,
|
|
1813
|
+
getCachedLoadingBgColor,
|
|
1814
|
+
isSeoActive,
|
|
1815
|
+
mode,
|
|
1816
|
+
nonLocalizedPaths,
|
|
1817
|
+
readCriticalCache,
|
|
1818
|
+
routing,
|
|
1819
|
+
setAndApplyDomRules,
|
|
1820
|
+
setBrandingEnabled,
|
|
1821
|
+
setCachedBrandingEnabled,
|
|
1822
|
+
setCachedLoadingBgColor,
|
|
1823
|
+
setEntitlements,
|
|
1824
|
+
toTranslations,
|
|
1825
|
+
writeCriticalCache
|
|
1826
|
+
]
|
|
1827
|
+
);
|
|
1828
|
+
useEffect2(() => {
|
|
1829
|
+
return () => {
|
|
1830
|
+
if (retryTimeoutRef.current) clearTimeout(retryTimeoutRef.current);
|
|
1831
|
+
if (loadingFailsafeTimeoutRef.current != null) window.clearTimeout(loadingFailsafeTimeoutRef.current);
|
|
1832
|
+
};
|
|
1833
|
+
}, []);
|
|
1834
|
+
return { isLoading, isNavigatingRef, loadData };
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// src/hooks/provider/useNavigationPrefetch.ts
|
|
1838
|
+
import { useEffect as useEffect3 } from "react";
|
|
1839
|
+
function useNavigationPrefetch({
|
|
1840
|
+
resolvedApiKey,
|
|
1841
|
+
apiBase,
|
|
1842
|
+
defaultLocale,
|
|
1843
|
+
locale,
|
|
1844
|
+
routing,
|
|
1845
|
+
allLocales,
|
|
1846
|
+
enhancedPathConfig
|
|
1847
|
+
}) {
|
|
1848
|
+
useEffect3(() => {
|
|
1849
|
+
if (!resolvedApiKey) return;
|
|
1850
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
1851
|
+
const connection = navigator?.connection;
|
|
1852
|
+
if (connection?.saveData) return;
|
|
1853
|
+
if (typeof connection?.effectiveType === "string" && /(^|-)2g$/.test(connection.effectiveType)) return;
|
|
1854
|
+
const prefetched = /* @__PURE__ */ new Set();
|
|
1855
|
+
const maxPrefetch = 40;
|
|
1856
|
+
const isAssetPath = (pathname) => {
|
|
1857
|
+
if (pathname === "/robots.txt" || pathname === "/sitemap.xml") return true;
|
|
1858
|
+
if (pathname.startsWith("/.well-known/")) return true;
|
|
1859
|
+
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);
|
|
1860
|
+
};
|
|
1861
|
+
const pickLocaleForUrl = (url) => {
|
|
1862
|
+
if (routing === "path") {
|
|
1863
|
+
const segment = url.pathname.split("/")[1] || "";
|
|
1864
|
+
if (segment && allLocales.includes(segment)) return segment;
|
|
1865
|
+
return locale;
|
|
1866
|
+
}
|
|
1867
|
+
const q = url.searchParams.get("t") || url.searchParams.get("locale");
|
|
1868
|
+
if (q && allLocales.includes(q)) return q;
|
|
1869
|
+
return locale;
|
|
1870
|
+
};
|
|
1871
|
+
const onIntent = (event) => {
|
|
1872
|
+
if (prefetched.size >= maxPrefetch) return;
|
|
1873
|
+
const target = event.target;
|
|
1874
|
+
const anchor = target?.closest?.("a[href]");
|
|
1875
|
+
if (!anchor) return;
|
|
1876
|
+
const href = anchor.getAttribute("href") || "";
|
|
1877
|
+
if (!href || /^(?:#|mailto:|tel:|sms:|javascript:)/i.test(href)) return;
|
|
1878
|
+
let url;
|
|
1879
|
+
try {
|
|
1880
|
+
url = new URL(href, window.location.origin);
|
|
1881
|
+
} catch {
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
if (url.origin !== window.location.origin) return;
|
|
1885
|
+
if (isAssetPath(url.pathname)) return;
|
|
1886
|
+
const targetLocale = pickLocaleForUrl(url);
|
|
1887
|
+
if (!targetLocale || targetLocale === defaultLocale) return;
|
|
1888
|
+
const normalizedPath = processPath(url.pathname, enhancedPathConfig);
|
|
1889
|
+
const key = `${targetLocale}:${normalizedPath}`;
|
|
1890
|
+
if (prefetched.has(key)) return;
|
|
1891
|
+
prefetched.add(key);
|
|
1892
|
+
const pathParam = `${url.pathname}${url.search}`;
|
|
1893
|
+
const bootstrapUrl = `${apiBase}/functions/v1/bootstrap?key=${encodeURIComponent(resolvedApiKey)}&locale=${encodeURIComponent(targetLocale)}&path=${encodeURIComponent(pathParam)}`;
|
|
1894
|
+
const bundleUrl = `${apiBase}/functions/v1/bundle?key=${encodeURIComponent(resolvedApiKey)}&locale=${encodeURIComponent(targetLocale)}&path=${encodeURIComponent(pathParam)}&scoped=1`;
|
|
1895
|
+
void fetch(bootstrapUrl, { cache: "force-cache" }).catch(() => void 0);
|
|
1896
|
+
void fetch(bundleUrl, { cache: "force-cache" }).catch(() => void 0);
|
|
1897
|
+
};
|
|
1898
|
+
document.addEventListener("pointerover", onIntent, { passive: true });
|
|
1899
|
+
document.addEventListener("touchstart", onIntent, { passive: true });
|
|
1900
|
+
document.addEventListener("focusin", onIntent);
|
|
1901
|
+
return () => {
|
|
1902
|
+
document.removeEventListener("pointerover", onIntent);
|
|
1903
|
+
document.removeEventListener("touchstart", onIntent);
|
|
1904
|
+
document.removeEventListener("focusin", onIntent);
|
|
1905
|
+
};
|
|
1906
|
+
}, [allLocales, apiBase, defaultLocale, enhancedPathConfig, locale, resolvedApiKey, routing]);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// src/hooks/provider/useLinkAutoPrefix.ts
|
|
1910
|
+
import { useEffect as useEffect4 } from "react";
|
|
1911
|
+
function useLinkAutoPrefix({
|
|
1912
|
+
routing,
|
|
1913
|
+
autoPrefixLinks,
|
|
1914
|
+
allLocales,
|
|
1915
|
+
locale,
|
|
1916
|
+
navigateRef,
|
|
1917
|
+
nonLocalizedPaths
|
|
1918
|
+
}) {
|
|
1919
|
+
useEffect4(() => {
|
|
1920
|
+
if (routing !== "path") return;
|
|
1921
|
+
if (!autoPrefixLinks) return;
|
|
1922
|
+
const supportedLocales = allLocales;
|
|
1923
|
+
const shouldProcessCurrentPath = () => {
|
|
1924
|
+
const parts = window.location.pathname.split("/").filter(Boolean);
|
|
1925
|
+
return parts.length > 0 && supportedLocales.includes(parts[0]);
|
|
1926
|
+
};
|
|
1927
|
+
const buildLocalePrefixedPath = (rawHref) => {
|
|
1928
|
+
if (!rawHref) return null;
|
|
1929
|
+
const trimmed = rawHref.trim();
|
|
1930
|
+
if (!trimmed) return null;
|
|
1931
|
+
const isAbsolutePath = trimmed.startsWith("/");
|
|
1932
|
+
const isAbsoluteUrl = /^https?:\/\//i.test(trimmed) || trimmed.startsWith("//");
|
|
1933
|
+
if (!isAbsolutePath && !isAbsoluteUrl) return null;
|
|
1934
|
+
if (/^(?:#|mailto:|tel:|sms:|javascript:)/i.test(trimmed)) return null;
|
|
1935
|
+
let url;
|
|
1936
|
+
try {
|
|
1937
|
+
url = new URL(trimmed, window.location.origin);
|
|
1938
|
+
} catch {
|
|
1939
|
+
return null;
|
|
1940
|
+
}
|
|
1941
|
+
if (url.origin !== window.location.origin) return null;
|
|
1942
|
+
if (isNonLocalizedPath(url.pathname, nonLocalizedPaths)) return null;
|
|
1943
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
1944
|
+
if (parts.length === 0) {
|
|
1945
|
+
return `/${locale}${url.search}${url.hash}`;
|
|
1946
|
+
}
|
|
1947
|
+
if (supportedLocales.includes(parts[0])) return null;
|
|
1948
|
+
const pathWithoutLeadingSlashes = url.pathname.replace(/^\/+/, "");
|
|
1949
|
+
const nextPathname = pathWithoutLeadingSlashes ? `/${locale}/${pathWithoutLeadingSlashes}` : `/${locale}`;
|
|
1950
|
+
return `${nextPathname}${url.search}${url.hash}`;
|
|
1951
|
+
};
|
|
1952
|
+
const ORIGINAL_HREF_KEY = "data-Lovalingo-href-original";
|
|
1953
|
+
const patchAnchor = (a) => {
|
|
1954
|
+
if (!a || a.hasAttribute("data-Lovalingo-exclude")) return;
|
|
1955
|
+
const original = a.getAttribute(ORIGINAL_HREF_KEY) ?? a.getAttribute("href") ?? "";
|
|
1956
|
+
if (!a.getAttribute(ORIGINAL_HREF_KEY) && original) {
|
|
1957
|
+
a.setAttribute(ORIGINAL_HREF_KEY, original);
|
|
1958
|
+
}
|
|
1959
|
+
const fixed = buildLocalePrefixedPath(original);
|
|
1960
|
+
if (fixed) {
|
|
1961
|
+
if (a.getAttribute("href") !== fixed) a.setAttribute("href", fixed);
|
|
1962
|
+
} else if (original) {
|
|
1963
|
+
if (a.getAttribute("href") !== original) a.setAttribute("href", original);
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
const patchAllAnchors = () => {
|
|
1967
|
+
if (!shouldProcessCurrentPath()) return;
|
|
1968
|
+
document.querySelectorAll("a[href]").forEach((node) => {
|
|
1969
|
+
if (node instanceof HTMLAnchorElement) patchAnchor(node);
|
|
1970
|
+
});
|
|
1971
|
+
};
|
|
1972
|
+
patchAllAnchors();
|
|
1973
|
+
const mo = new MutationObserver((mutations) => {
|
|
1974
|
+
if (!shouldProcessCurrentPath()) return;
|
|
1975
|
+
for (const mutation of mutations) {
|
|
1976
|
+
mutation.addedNodes.forEach((node) => {
|
|
1977
|
+
if (!(node instanceof HTMLElement)) return;
|
|
1978
|
+
if (node instanceof HTMLAnchorElement) {
|
|
1979
|
+
patchAnchor(node);
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
node.querySelectorAll?.("a[href]").forEach((a) => {
|
|
1983
|
+
if (a instanceof HTMLAnchorElement) patchAnchor(a);
|
|
1984
|
+
});
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1988
|
+
mo.observe(document.body, { childList: true, subtree: true });
|
|
1989
|
+
const onClickCapture = (event) => {
|
|
1990
|
+
if (!shouldProcessCurrentPath()) return;
|
|
1991
|
+
if (event.defaultPrevented) return;
|
|
1992
|
+
if (event.button !== 0) return;
|
|
1993
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
1994
|
+
const target = event.target;
|
|
1995
|
+
const a = target?.closest?.("a[href]");
|
|
1996
|
+
if (!a) return;
|
|
1997
|
+
if (a.target && a.target !== "_self") return;
|
|
1998
|
+
if (a.hasAttribute("download")) return;
|
|
1999
|
+
if (a.getAttribute("rel")?.includes("external")) return;
|
|
2000
|
+
const original = a.getAttribute(ORIGINAL_HREF_KEY) ?? a.getAttribute("href") ?? "";
|
|
2001
|
+
const fixed = buildLocalePrefixedPath(original);
|
|
2002
|
+
if (!fixed) return;
|
|
2003
|
+
event.preventDefault();
|
|
2004
|
+
event.stopImmediatePropagation();
|
|
2005
|
+
event.stopPropagation();
|
|
2006
|
+
const navigate = navigateRef?.current;
|
|
2007
|
+
if (navigate) {
|
|
2008
|
+
navigate(fixed);
|
|
2009
|
+
} else {
|
|
2010
|
+
window.location.assign(fixed);
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
document.addEventListener("click", onClickCapture, true);
|
|
2014
|
+
return () => {
|
|
2015
|
+
mo.disconnect();
|
|
2016
|
+
document.removeEventListener("click", onClickCapture, true);
|
|
2017
|
+
};
|
|
2018
|
+
}, [routing, autoPrefixLinks, allLocales, locale, navigateRef, nonLocalizedPaths]);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// src/hooks/provider/usePageviewTracking.ts
|
|
2022
|
+
import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef4 } from "react";
|
|
2023
|
+
function usePageviewTracking({ apiRef, resolvedApiKey }) {
|
|
2024
|
+
const lastPageviewRef = useRef4("");
|
|
2025
|
+
const lastPageviewFingerprintRef = useRef4("");
|
|
2026
|
+
const pageviewFingerprintTimeoutRef = useRef4(null);
|
|
2027
|
+
const pageviewFingerprintRetryTimeoutRef = useRef4(null);
|
|
2028
|
+
useEffect5(() => {
|
|
2029
|
+
lastPageviewRef.current = "";
|
|
2030
|
+
lastPageviewFingerprintRef.current = "";
|
|
2031
|
+
}, [resolvedApiKey]);
|
|
2032
|
+
const trackPageviewOnce = useCallback4(
|
|
2033
|
+
(path) => {
|
|
2034
|
+
const next = (path || "").toString();
|
|
2035
|
+
if (!next) return;
|
|
2036
|
+
if (lastPageviewRef.current === next) return;
|
|
2037
|
+
lastPageviewRef.current = next;
|
|
2038
|
+
apiRef.current.trackPageview(next);
|
|
2039
|
+
const trySendFingerprint = () => {
|
|
2040
|
+
if (typeof window === "undefined") return;
|
|
2041
|
+
const markersReady = window.__lovalingoMarkersReady === true;
|
|
2042
|
+
if (!markersReady) return;
|
|
2043
|
+
const fp = getCriticalFingerprint();
|
|
2044
|
+
if (!fp || fp.critical_count <= 0) return;
|
|
2045
|
+
const signature = `${next}|${fp.critical_hash}|${fp.critical_count}`;
|
|
2046
|
+
if (lastPageviewFingerprintRef.current === signature) return;
|
|
2047
|
+
lastPageviewFingerprintRef.current = signature;
|
|
2048
|
+
apiRef.current.trackPageview(next, fp);
|
|
2049
|
+
};
|
|
2050
|
+
if (pageviewFingerprintTimeoutRef.current != null) window.clearTimeout(pageviewFingerprintTimeoutRef.current);
|
|
2051
|
+
if (pageviewFingerprintRetryTimeoutRef.current != null) window.clearTimeout(pageviewFingerprintRetryTimeoutRef.current);
|
|
2052
|
+
pageviewFingerprintTimeoutRef.current = window.setTimeout(trySendFingerprint, 800);
|
|
2053
|
+
pageviewFingerprintRetryTimeoutRef.current = window.setTimeout(trySendFingerprint, 2e3);
|
|
2054
|
+
},
|
|
2055
|
+
[apiRef]
|
|
2056
|
+
);
|
|
2057
|
+
return { trackPageviewOnce };
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// src/hooks/provider/useSitemapLinkTag.ts
|
|
2061
|
+
import { useEffect as useEffect6 } from "react";
|
|
2062
|
+
function useSitemapLinkTag({ enabled, resolvedApiKey, isSeoActive }) {
|
|
2063
|
+
useEffect6(() => {
|
|
2064
|
+
if (enabled && resolvedApiKey && isSeoActive()) {
|
|
2065
|
+
const sitemapUrl = `${window.location.origin}/sitemap.xml`;
|
|
2066
|
+
const existingLink = document.querySelector(`link[rel="sitemap"][href="${sitemapUrl}"]`);
|
|
2067
|
+
if (existingLink) return;
|
|
2068
|
+
const link = document.createElement("link");
|
|
2069
|
+
link.rel = "sitemap";
|
|
2070
|
+
link.type = "application/xml";
|
|
2071
|
+
link.href = sitemapUrl;
|
|
2072
|
+
document.head.appendChild(link);
|
|
2073
|
+
return () => {
|
|
2074
|
+
const linkToRemove = document.querySelector(`link[rel="sitemap"][href="${sitemapUrl}"]`);
|
|
2075
|
+
if (linkToRemove) {
|
|
2076
|
+
document.head.removeChild(linkToRemove);
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
}, [enabled, resolvedApiKey, isSeoActive]);
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// src/hooks/provider/useStringMissReporting.ts
|
|
2084
|
+
import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef5 } from "react";
|
|
2085
|
+
var MISS_SCAN_THROTTLE_MS = 600;
|
|
2086
|
+
var MISS_MAX_PER_PAGE = 500;
|
|
2087
|
+
function looksLike404Page() {
|
|
2088
|
+
if (typeof document === "undefined") return false;
|
|
2089
|
+
const meta = document.querySelector('meta[name="lovalingo:status"]');
|
|
2090
|
+
if (meta && meta.getAttribute("content") === "404") return true;
|
|
2091
|
+
const title = (document.title || "").toLowerCase();
|
|
2092
|
+
if (/page not found|seite nicht gefunden|article not found|error 404|page missing|does not exist/.test(title)) return true;
|
|
2093
|
+
const h1 = document.querySelector("h1");
|
|
2094
|
+
if (h1) {
|
|
2095
|
+
const txt = (h1.innerText || "").toLowerCase();
|
|
2096
|
+
if (/page not found|seite nicht gefunden|article not found|error 404/.test(txt)) return true;
|
|
2097
|
+
if (txt.includes("404") && txt.length < 50) return true;
|
|
2098
|
+
}
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
function useStringMissReporting(args) {
|
|
2102
|
+
const pendingRef = useRef5(/* @__PURE__ */ new Set());
|
|
2103
|
+
const seenRef = useRef5(/* @__PURE__ */ new Set());
|
|
2104
|
+
const scheduledRef = useRef5(null);
|
|
2105
|
+
const inFlightRef = useRef5(false);
|
|
2106
|
+
useEffect7(() => {
|
|
2107
|
+
pendingRef.current.clear();
|
|
2108
|
+
seenRef.current.clear();
|
|
2109
|
+
}, [args.locale]);
|
|
2110
|
+
const shouldSkip = useCallback5(() => {
|
|
2111
|
+
if (typeof window === "undefined" || typeof document === "undefined") return true;
|
|
2112
|
+
if (looksLike404Page()) return true;
|
|
2113
|
+
const disableLiveMisses = Boolean(window.__lovalingoDisableMisses);
|
|
2114
|
+
if (disableLiveMisses) return true;
|
|
2115
|
+
if (!args.resolvedApiKey || args.mode !== "dom") return true;
|
|
2116
|
+
if (args.isLoading) return true;
|
|
2117
|
+
if (args.locale === args.defaultLocale) return true;
|
|
2118
|
+
if (args.routing === "path") {
|
|
2119
|
+
const stripped = stripLocalePrefix(window.location.pathname, args.allLocales);
|
|
2120
|
+
if (isNonLocalizedPath(stripped, args.nonLocalizedPaths)) return true;
|
|
2121
|
+
}
|
|
2122
|
+
return false;
|
|
2123
|
+
}, [args.allLocales, args.defaultLocale, args.isLoading, args.locale, args.mode, args.nonLocalizedPaths, args.resolvedApiKey, args.routing]);
|
|
2124
|
+
const runScan = useCallback5(async () => {
|
|
2125
|
+
scheduledRef.current = null;
|
|
2126
|
+
if (shouldSkip()) return;
|
|
2127
|
+
if (!document.body) return;
|
|
2128
|
+
if (inFlightRef.current) return;
|
|
2129
|
+
const ignore = /* @__PURE__ */ new Set([...pendingRef.current, ...seenRef.current]);
|
|
2130
|
+
const { misses } = scanDomForMisses({ max: MISS_MAX_PER_PAGE, ignore });
|
|
2131
|
+
if (misses.length === 0) return;
|
|
2132
|
+
logDebug(`[Lovalingo] Live misses detected: ${misses.length}`);
|
|
2133
|
+
misses.forEach((miss) => pendingRef.current.add(miss.source_text));
|
|
2134
|
+
inFlightRef.current = true;
|
|
2135
|
+
try {
|
|
2136
|
+
const response = await args.apiRef.current.reportStringMisses(args.locale, misses, {
|
|
2137
|
+
sourceLocale: args.defaultLocale,
|
|
2138
|
+
locales: args.allLocales
|
|
2139
|
+
});
|
|
2140
|
+
if (response?.ignored) {
|
|
2141
|
+
for (const miss of misses) {
|
|
2142
|
+
pendingRef.current.delete(miss.source_text);
|
|
2143
|
+
seenRef.current.add(miss.source_text);
|
|
2144
|
+
}
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
const translations = Array.isArray(response?.translations) ? response.translations : [];
|
|
2148
|
+
const pii = Array.isArray(response?.pii) ? response.pii : [];
|
|
2149
|
+
logDebug(`[Lovalingo] Live misses resolved`, { translations: translations.length, pii: pii.length });
|
|
2150
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
2151
|
+
pii.forEach((text) => resolved.add(text));
|
|
2152
|
+
translations.forEach((row) => {
|
|
2153
|
+
if (row?.source_text) resolved.add(row.source_text);
|
|
2154
|
+
});
|
|
2155
|
+
if (translations.length > 0) {
|
|
2156
|
+
const additions = translations.map((row) => ({
|
|
2157
|
+
source_text: row.source_text,
|
|
2158
|
+
translated_text: row.translated_text,
|
|
2159
|
+
source_locale: args.defaultLocale,
|
|
2160
|
+
target_locale: args.locale
|
|
2161
|
+
})).filter((row) => row.source_text && row.translated_text);
|
|
2162
|
+
if (additions.length > 0) {
|
|
2163
|
+
addActiveTranslations(additions);
|
|
2164
|
+
applyActiveTranslations(document.body);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
for (const miss of misses) {
|
|
2168
|
+
pendingRef.current.delete(miss.source_text);
|
|
2169
|
+
if (resolved.has(miss.source_text)) {
|
|
2170
|
+
seenRef.current.add(miss.source_text);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
} catch {
|
|
2174
|
+
for (const miss of misses) {
|
|
2175
|
+
pendingRef.current.delete(miss.source_text);
|
|
2176
|
+
}
|
|
2177
|
+
} finally {
|
|
2178
|
+
inFlightRef.current = false;
|
|
2179
|
+
}
|
|
2180
|
+
}, [args.apiRef, args.defaultLocale, args.locale, shouldSkip]);
|
|
2181
|
+
const scheduleScan2 = useCallback5(() => {
|
|
2182
|
+
if (scheduledRef.current != null) return;
|
|
2183
|
+
scheduledRef.current = window.setTimeout(() => {
|
|
2184
|
+
void runScan();
|
|
2185
|
+
}, MISS_SCAN_THROTTLE_MS);
|
|
2186
|
+
}, [runScan]);
|
|
2187
|
+
useEffect7(() => {
|
|
2188
|
+
if (shouldSkip()) return;
|
|
2189
|
+
scheduleScan2();
|
|
2190
|
+
if (!document.body) return;
|
|
2191
|
+
const observer2 = new MutationObserver(() => scheduleScan2());
|
|
2192
|
+
observer2.observe(document.body, {
|
|
2193
|
+
childList: true,
|
|
2194
|
+
subtree: true,
|
|
2195
|
+
characterData: true
|
|
2196
|
+
});
|
|
2197
|
+
return () => {
|
|
2198
|
+
observer2.disconnect();
|
|
2199
|
+
if (scheduledRef.current != null) {
|
|
2200
|
+
window.clearTimeout(scheduledRef.current);
|
|
2201
|
+
scheduledRef.current = null;
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
}, [scheduleScan2, shouldSkip]);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// src/components/LanguageSwitcher.tsx
|
|
2208
|
+
import React2, { useState as useState2, useEffect as useEffect8, useMemo, useRef as useRef6 } from "react";
|
|
2209
|
+
|
|
2210
|
+
// src/utils/languageFlags.ts
|
|
2211
|
+
var EXACT_LOCALE_FLAG_OVERRIDES = {
|
|
2212
|
+
en: "\u{1F1EC}\u{1F1E7}",
|
|
2213
|
+
ar: "\u{1F1F8}\u{1F1E6}",
|
|
2214
|
+
zh: "\u{1F1E8}\u{1F1F3}",
|
|
2215
|
+
fa: "\u{1F1EE}\u{1F1F7}",
|
|
2216
|
+
he: "\u{1F1EE}\u{1F1F1}"
|
|
2217
|
+
};
|
|
2218
|
+
var LANGUAGE_DEFAULT_REGION = {
|
|
2219
|
+
ar: "SA",
|
|
2220
|
+
bn: "BD",
|
|
2221
|
+
cs: "CZ",
|
|
2222
|
+
da: "DK",
|
|
2223
|
+
de: "DE",
|
|
2224
|
+
el: "GR",
|
|
2225
|
+
en: "GB",
|
|
2226
|
+
es: "ES",
|
|
2227
|
+
fa: "IR",
|
|
2228
|
+
fi: "FI",
|
|
2229
|
+
fr: "FR",
|
|
2230
|
+
he: "IL",
|
|
2231
|
+
hi: "IN",
|
|
2232
|
+
hu: "HU",
|
|
2233
|
+
hy: "AM",
|
|
2234
|
+
id: "ID",
|
|
2235
|
+
it: "IT",
|
|
2236
|
+
ja: "JP",
|
|
2237
|
+
ko: "KR",
|
|
2238
|
+
nl: "NL",
|
|
2239
|
+
no: "NO",
|
|
2240
|
+
pl: "PL",
|
|
2241
|
+
pt: "PT",
|
|
2242
|
+
ro: "RO",
|
|
2243
|
+
ru: "RU",
|
|
2244
|
+
sk: "SK",
|
|
2245
|
+
sv: "SE",
|
|
2246
|
+
th: "TH",
|
|
2247
|
+
tr: "TR",
|
|
2248
|
+
uk: "UA",
|
|
2249
|
+
vi: "VN",
|
|
2250
|
+
yo: "NG",
|
|
2251
|
+
zh: "CN"
|
|
2252
|
+
};
|
|
2253
|
+
function normalizeLocaleCode(locale) {
|
|
2254
|
+
if (typeof locale !== "string") return "";
|
|
2255
|
+
return locale.trim().replace(/_/g, "-").toLowerCase();
|
|
2256
|
+
}
|
|
2257
|
+
function parseLocale(locale) {
|
|
2258
|
+
const normalized = normalizeLocaleCode(locale);
|
|
2259
|
+
if (!normalized) return { language: "", region: null };
|
|
2260
|
+
const parts = normalized.split("-").filter(Boolean);
|
|
2261
|
+
const language = parts[0] || "";
|
|
2262
|
+
const regionPart = parts.find((part, index) => index > 0 && /^[a-z]{2}$/.test(part));
|
|
2263
|
+
const region = regionPart ? regionPart.toUpperCase() : null;
|
|
2264
|
+
return { language, region };
|
|
2265
|
+
}
|
|
2266
|
+
function countryCodeToFlagEmoji(countryCode) {
|
|
2267
|
+
if (typeof countryCode !== "string") return null;
|
|
2268
|
+
const normalized = countryCode.trim().toUpperCase();
|
|
2269
|
+
if (!/^[A-Z]{2}$/.test(normalized)) return null;
|
|
2270
|
+
const first = normalized.charCodeAt(0) + 127397;
|
|
2271
|
+
const second = normalized.charCodeAt(1) + 127397;
|
|
2272
|
+
return String.fromCodePoint(first, second);
|
|
2273
|
+
}
|
|
2274
|
+
function resolveLocaleFlag(locale) {
|
|
2275
|
+
const normalized = normalizeLocaleCode(locale);
|
|
2276
|
+
if (!normalized) return "\u{1F310}";
|
|
2277
|
+
const exact = EXACT_LOCALE_FLAG_OVERRIDES[normalized];
|
|
2278
|
+
if (exact) return exact;
|
|
2279
|
+
const { language, region } = parseLocale(normalized);
|
|
2280
|
+
if (!language) return "\u{1F310}";
|
|
2281
|
+
if (region) {
|
|
2282
|
+
const regionFlag = countryCodeToFlagEmoji(region);
|
|
2283
|
+
if (regionFlag) return regionFlag;
|
|
2284
|
+
}
|
|
2285
|
+
const defaultRegion = LANGUAGE_DEFAULT_REGION[language];
|
|
2286
|
+
if (defaultRegion) {
|
|
2287
|
+
const defaultFlag = countryCodeToFlagEmoji(defaultRegion);
|
|
2288
|
+
if (defaultFlag) return defaultFlag;
|
|
2289
|
+
}
|
|
2290
|
+
return "\u{1F310}";
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
// src/components/LanguageSwitcher.tsx
|
|
2294
|
+
var LanguageSwitcher = ({
|
|
2295
|
+
locales,
|
|
2296
|
+
currentLocale,
|
|
2297
|
+
onLocaleChange,
|
|
2298
|
+
position = "bottom-right",
|
|
2299
|
+
offsetY = 20,
|
|
2300
|
+
theme = "dark",
|
|
2301
|
+
branding
|
|
2302
|
+
}) => {
|
|
2303
|
+
const [isOpen, setIsOpen] = useState2(false);
|
|
2304
|
+
const [isMobile, setIsMobile] = useState2(false);
|
|
2305
|
+
const containerRef = useRef6(null);
|
|
2306
|
+
const isRight = position.endsWith("right");
|
|
2307
|
+
const isTop = position.startsWith("top");
|
|
2308
|
+
const isBottom = position.startsWith("bottom");
|
|
2309
|
+
const tokens = theme === "light" ? {
|
|
2310
|
+
surfaceBg: "rgba(255, 255, 255, 0.93)",
|
|
2311
|
+
surfaceHoverBg: "rgba(245, 245, 245, 0.95)",
|
|
2312
|
+
text: "rgba(13, 13, 13, 0.96)",
|
|
2313
|
+
textMuted: "rgba(13, 13, 13, 0.74)",
|
|
2314
|
+
divider: "rgba(0, 0, 0, 0.10)",
|
|
2315
|
+
insetHighlight: "inset 0 0 1px rgba(0, 0, 0, 0.10)",
|
|
2316
|
+
tabShadowRight: "-2px 0 8px rgba(0, 0, 0, 0.12), inset 0 0 1px rgba(0, 0, 0, 0.10)",
|
|
2317
|
+
tabShadowLeft: "2px 0 8px rgba(0, 0, 0, 0.12), inset 0 0 1px rgba(0, 0, 0, 0.10)",
|
|
2318
|
+
tabHoverShadowRight: "-4px 0 16px rgba(0, 0, 0, 0.16), inset 0 0 1px rgba(0, 0, 0, 0.12)",
|
|
2319
|
+
tabHoverShadowLeft: "4px 0 16px rgba(0, 0, 0, 0.16), inset 0 0 1px rgba(0, 0, 0, 0.12)",
|
|
2320
|
+
panelShadow: "0 8px 24px rgba(0, 0, 0, 0.14), inset 0 0 1px rgba(0, 0, 0, 0.10)"
|
|
2321
|
+
} : {
|
|
2322
|
+
surfaceBg: "rgba(26, 26, 26, 0.93)",
|
|
2323
|
+
surfaceHoverBg: "rgba(35, 35, 35, 0.95)",
|
|
2324
|
+
text: "rgba(255, 255, 255, 0.98)",
|
|
2325
|
+
textMuted: "rgba(255, 255, 255, 0.82)",
|
|
2326
|
+
divider: "rgba(255, 255, 255, 0.12)",
|
|
2327
|
+
insetHighlight: "inset 0 0 1px rgba(255, 255, 255, 0.10)",
|
|
2328
|
+
tabShadowRight: "-2px 0 8px rgba(0, 0, 0, 0.2), inset 0 0 1px rgba(255, 255, 255, 0.10)",
|
|
2329
|
+
tabShadowLeft: "2px 0 8px rgba(0, 0, 0, 0.2), inset 0 0 1px rgba(255, 255, 255, 0.10)",
|
|
2330
|
+
tabHoverShadowRight: "-4px 0 16px rgba(0, 0, 0, 0.3), inset 0 0 1px rgba(255, 255, 255, 0.15)",
|
|
2331
|
+
tabHoverShadowLeft: "4px 0 16px rgba(0, 0, 0, 0.3), inset 0 0 1px rgba(255, 255, 255, 0.15)",
|
|
2332
|
+
panelShadow: "0 8px 24px rgba(0, 0, 0, 0.3), inset 0 0 1px rgba(255, 255, 255, 0.10)"
|
|
2333
|
+
};
|
|
2334
|
+
const normalizedCurrentLocale = useMemo(() => normalizeLocaleCode(currentLocale), [currentLocale]);
|
|
2335
|
+
const orderedLocales = useMemo(() => {
|
|
2336
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2337
|
+
const items = [];
|
|
2338
|
+
const pushLocale = (value) => {
|
|
2339
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
2340
|
+
const normalized = normalizeLocaleCode(raw);
|
|
2341
|
+
if (!raw || !normalized || seen.has(normalized)) return;
|
|
2342
|
+
seen.add(normalized);
|
|
2343
|
+
items.push({ raw, normalized });
|
|
2344
|
+
};
|
|
2345
|
+
if (normalizedCurrentLocale) {
|
|
2346
|
+
pushLocale(currentLocale);
|
|
2347
|
+
}
|
|
2348
|
+
for (const locale of locales) {
|
|
2349
|
+
const raw = typeof locale === "string" ? locale.trim() : "";
|
|
2350
|
+
const normalized = normalizeLocaleCode(raw);
|
|
2351
|
+
if (!normalized || normalized === normalizedCurrentLocale) continue;
|
|
2352
|
+
pushLocale(raw);
|
|
2353
|
+
}
|
|
2354
|
+
if (items.length === 0) {
|
|
2355
|
+
for (const locale of locales) pushLocale(locale);
|
|
2356
|
+
}
|
|
2357
|
+
return items;
|
|
2358
|
+
}, [currentLocale, locales, normalizedCurrentLocale]);
|
|
2359
|
+
const tabFlag = useMemo(() => {
|
|
2360
|
+
const fallbackLocale = orderedLocales[0]?.normalized || normalizedCurrentLocale;
|
|
2361
|
+
return resolveLocaleFlag(fallbackLocale);
|
|
2362
|
+
}, [orderedLocales, normalizedCurrentLocale]);
|
|
2363
|
+
useEffect8(() => {
|
|
2364
|
+
const handleClickOutside = (event) => {
|
|
2365
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
2366
|
+
setIsOpen(false);
|
|
2367
|
+
}
|
|
2368
|
+
};
|
|
2369
|
+
if (isOpen) {
|
|
2370
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
2371
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
2372
|
+
}
|
|
2373
|
+
}, [isOpen]);
|
|
2374
|
+
useEffect8(() => {
|
|
2375
|
+
if (typeof window === "undefined") return;
|
|
2376
|
+
const query = "(max-width: 640px)";
|
|
2377
|
+
const media = window.matchMedia(query);
|
|
2378
|
+
const update = () => setIsMobile(media.matches);
|
|
2379
|
+
update();
|
|
2380
|
+
if (typeof media.addEventListener === "function") {
|
|
2381
|
+
media.addEventListener("change", update);
|
|
2382
|
+
return () => media.removeEventListener("change", update);
|
|
2383
|
+
}
|
|
2384
|
+
media.addListener(update);
|
|
2385
|
+
return () => media.removeListener(update);
|
|
2386
|
+
}, []);
|
|
2387
|
+
useEffect8(() => {
|
|
2388
|
+
const handleKeyDown = (event) => {
|
|
2389
|
+
if (event.key === "Escape" && isOpen) {
|
|
2390
|
+
setIsOpen(false);
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
if (isOpen) {
|
|
2394
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
2395
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
2396
|
+
}
|
|
2397
|
+
}, [isOpen]);
|
|
2398
|
+
const containerStyles = {
|
|
2399
|
+
position: "fixed",
|
|
2400
|
+
zIndex: 9999,
|
|
2401
|
+
...isTop && { top: `calc(env(safe-area-inset-top, 0px) + 24px + ${offsetY}px)` },
|
|
2402
|
+
...isBottom && { bottom: `calc(env(safe-area-inset-bottom, 0px) + 24px + ${offsetY}px)` },
|
|
2403
|
+
...isRight && { right: 0 },
|
|
2404
|
+
...!isRight && { left: 0 },
|
|
2405
|
+
pointerEvents: "none"
|
|
2406
|
+
};
|
|
2407
|
+
const rootStyles = {
|
|
2408
|
+
position: "relative",
|
|
2409
|
+
display: "flex",
|
|
2410
|
+
alignItems: "center",
|
|
2411
|
+
pointerEvents: "none"
|
|
2412
|
+
};
|
|
2413
|
+
const tabStyles = {
|
|
2414
|
+
pointerEvents: "auto",
|
|
2415
|
+
display: "flex",
|
|
2416
|
+
alignItems: "center",
|
|
2417
|
+
justifyContent: "center",
|
|
2418
|
+
width: "44px",
|
|
2419
|
+
height: "50px",
|
|
2420
|
+
borderRadius: isRight ? "12px 0 0 12px" : "0 12px 12px 0",
|
|
2421
|
+
background: tokens.surfaceBg,
|
|
2422
|
+
boxShadow: isRight ? tokens.tabShadowRight : tokens.tabShadowLeft,
|
|
2423
|
+
cursor: "pointer",
|
|
2424
|
+
fontSize: "20px",
|
|
2425
|
+
transition: "all 0.2s ease",
|
|
2426
|
+
userSelect: "none",
|
|
2427
|
+
border: "none",
|
|
2428
|
+
color: tokens.text
|
|
2429
|
+
};
|
|
2430
|
+
const panelStyles = {
|
|
2431
|
+
position: "absolute",
|
|
2432
|
+
...isRight && { right: "48px" },
|
|
2433
|
+
...!isRight && { left: "48px" },
|
|
2434
|
+
top: "50%",
|
|
2435
|
+
transform: isOpen ? "translateY(-50%) translateX(0)" : `translateY(-50%) translateX(${isRight ? "12px" : "-12px"})`,
|
|
2436
|
+
opacity: isOpen ? 1 : 0,
|
|
2437
|
+
pointerEvents: isOpen ? "auto" : "none",
|
|
2438
|
+
background: tokens.surfaceBg,
|
|
2439
|
+
borderRadius: "16px",
|
|
2440
|
+
padding: "10px 12px",
|
|
2441
|
+
display: "flex",
|
|
2442
|
+
flexDirection: "column",
|
|
2443
|
+
gap: "10px",
|
|
2444
|
+
boxShadow: tokens.panelShadow,
|
|
2445
|
+
transition: "opacity 0.25s ease, transform 0.25s ease",
|
|
2446
|
+
width: isMobile ? "min(320px, calc(100vw - 88px))" : "auto"
|
|
2447
|
+
};
|
|
2448
|
+
const localeRowStyles = {
|
|
2449
|
+
display: isMobile ? "grid" : "flex",
|
|
2450
|
+
gridTemplateColumns: isMobile ? "repeat(5, minmax(0, 1fr))" : void 0,
|
|
2451
|
+
justifyItems: isMobile ? "center" : void 0,
|
|
2452
|
+
gap: "10px",
|
|
2453
|
+
padding: "0 2px"
|
|
2454
|
+
};
|
|
2455
|
+
const badgeRowStyles = {
|
|
2456
|
+
display: "flex",
|
|
2457
|
+
alignItems: "center",
|
|
2458
|
+
gap: "8px",
|
|
2459
|
+
paddingTop: "8px",
|
|
2460
|
+
borderTop: `1px solid ${tokens.divider}`,
|
|
2461
|
+
fontSize: "12px",
|
|
2462
|
+
color: tokens.textMuted,
|
|
2463
|
+
userSelect: "none",
|
|
2464
|
+
whiteSpace: "nowrap"
|
|
2465
|
+
};
|
|
2466
|
+
const badgeLinkStyles = {
|
|
2467
|
+
color: tokens.text,
|
|
2468
|
+
textDecoration: "none",
|
|
2469
|
+
display: "inline-flex",
|
|
2470
|
+
alignItems: "center",
|
|
2471
|
+
gap: "6px"
|
|
2472
|
+
};
|
|
2473
|
+
const flagButtonStyles = (isActive) => ({
|
|
2474
|
+
// Why: the panel stays mounted for the close animation, so buttons must be non-interactive while hidden.
|
|
2475
|
+
pointerEvents: isOpen ? "auto" : "none",
|
|
2476
|
+
width: "32px",
|
|
2477
|
+
height: "32px",
|
|
2478
|
+
borderRadius: "50%",
|
|
2479
|
+
display: "flex",
|
|
2480
|
+
alignItems: "center",
|
|
2481
|
+
justifyContent: "center",
|
|
2482
|
+
fontSize: "20px",
|
|
2483
|
+
background: isActive ? "rgba(59, 130, 246, 0.2)" : "transparent",
|
|
2484
|
+
border: isActive ? "2px solid rgb(59, 130, 246)" : "2px solid transparent",
|
|
2485
|
+
cursor: "pointer",
|
|
2486
|
+
transition: "transform 0.15s ease, filter 0.15s ease, background 0.15s ease",
|
|
2487
|
+
flexShrink: 0,
|
|
2488
|
+
userSelect: "none",
|
|
2489
|
+
padding: 0
|
|
2490
|
+
});
|
|
2491
|
+
return /* @__PURE__ */ React2.createElement("div", { ref: containerRef, style: containerStyles, "data-Lovalingo-exclude": "true" }, /* @__PURE__ */ React2.createElement("div", { style: rootStyles }, /* @__PURE__ */ React2.createElement(
|
|
2492
|
+
"button",
|
|
2493
|
+
{
|
|
2494
|
+
style: tabStyles,
|
|
2495
|
+
onClick: () => setIsOpen(!isOpen),
|
|
2496
|
+
onMouseEnter: (e) => {
|
|
2497
|
+
e.currentTarget.style.background = tokens.surfaceHoverBg;
|
|
2498
|
+
e.currentTarget.style.boxShadow = isRight ? tokens.tabHoverShadowRight : tokens.tabHoverShadowLeft;
|
|
2499
|
+
},
|
|
2500
|
+
onMouseLeave: (e) => {
|
|
2501
|
+
e.currentTarget.style.background = tokens.surfaceBg;
|
|
2502
|
+
e.currentTarget.style.boxShadow = isRight ? tokens.tabShadowRight : tokens.tabShadowLeft;
|
|
2503
|
+
},
|
|
2504
|
+
"aria-label": "Open language switcher",
|
|
2505
|
+
"aria-expanded": isOpen
|
|
2506
|
+
},
|
|
2507
|
+
tabFlag
|
|
2508
|
+
), /* @__PURE__ */ React2.createElement("div", { style: panelStyles, role: "toolbar", "aria-label": "Language options" }, /* @__PURE__ */ React2.createElement("div", { style: localeRowStyles }, orderedLocales.map((entry) => {
|
|
2509
|
+
const isActive = entry.normalized === normalizedCurrentLocale;
|
|
2510
|
+
return /* @__PURE__ */ React2.createElement(
|
|
2511
|
+
"button",
|
|
2512
|
+
{
|
|
2513
|
+
key: entry.normalized,
|
|
2514
|
+
style: flagButtonStyles(isActive),
|
|
2515
|
+
onClick: (e) => {
|
|
2516
|
+
e.stopPropagation();
|
|
2517
|
+
if (isActive) {
|
|
2518
|
+
setIsOpen(false);
|
|
2519
|
+
} else {
|
|
2520
|
+
onLocaleChange(entry.raw);
|
|
2521
|
+
setIsOpen(false);
|
|
2522
|
+
}
|
|
2523
|
+
},
|
|
2524
|
+
onMouseEnter: (e) => {
|
|
2525
|
+
if (!isActive) {
|
|
2526
|
+
e.currentTarget.style.filter = "brightness(1.3)";
|
|
2527
|
+
}
|
|
2528
|
+
e.currentTarget.style.transform = "scale(1.1)";
|
|
2529
|
+
},
|
|
2530
|
+
onMouseLeave: (e) => {
|
|
2531
|
+
e.currentTarget.style.filter = "brightness(1)";
|
|
2532
|
+
e.currentTarget.style.transform = "scale(1)";
|
|
2533
|
+
},
|
|
2534
|
+
"aria-label": `Switch to ${entry.normalized.toUpperCase()}`,
|
|
2535
|
+
title: entry.normalized.toUpperCase(),
|
|
2536
|
+
tabIndex: isOpen ? 0 : -1
|
|
2537
|
+
},
|
|
2538
|
+
resolveLocaleFlag(entry.normalized)
|
|
2539
|
+
);
|
|
2540
|
+
})), (branding?.required || branding?.enabled) && /* @__PURE__ */ React2.createElement("div", { style: badgeRowStyles, "aria-label": "Lovalingo branding" }, /* @__PURE__ */ React2.createElement(
|
|
2541
|
+
"a",
|
|
2542
|
+
{
|
|
2543
|
+
href: branding.href || "https://lovalingo.com",
|
|
2544
|
+
target: "_blank",
|
|
2545
|
+
rel: "noreferrer",
|
|
2546
|
+
style: { ...badgeLinkStyles, pointerEvents: isOpen ? "auto" : "none" },
|
|
2547
|
+
tabIndex: isOpen ? 0 : -1,
|
|
2548
|
+
"aria-label": "Localized by Lovalingo",
|
|
2549
|
+
title: "Localized by Lovalingo"
|
|
2550
|
+
},
|
|
2551
|
+
/* @__PURE__ */ React2.createElement(
|
|
2552
|
+
"span",
|
|
2553
|
+
{
|
|
2554
|
+
style: {
|
|
2555
|
+
width: "16px",
|
|
2556
|
+
height: "16px",
|
|
2557
|
+
borderRadius: "999px",
|
|
2558
|
+
overflow: "hidden",
|
|
2559
|
+
background: "#DA2576",
|
|
2560
|
+
display: "inline-flex",
|
|
2561
|
+
alignItems: "center",
|
|
2562
|
+
justifyContent: "center",
|
|
2563
|
+
boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.25)",
|
|
2564
|
+
flexShrink: 0
|
|
2565
|
+
}
|
|
2566
|
+
},
|
|
2567
|
+
/* @__PURE__ */ React2.createElement("svg", { width: "16", height: "16", viewBox: "0 0 512 512", fill: "none", "aria-hidden": "true" }, /* @__PURE__ */ React2.createElement(
|
|
2568
|
+
"path",
|
|
2569
|
+
{
|
|
2570
|
+
d: "M256 480C379.712 480 480 379.712 480 256C480 132.288 379.712 32 256 32C132.288 32 32 132.288 32 256C32 379.712 132.288 480 256 480Z",
|
|
2571
|
+
fill: "#DA2576"
|
|
2572
|
+
}
|
|
2573
|
+
), /* @__PURE__ */ React2.createElement(
|
|
2574
|
+
"path",
|
|
2575
|
+
{
|
|
2576
|
+
d: "M226.321 415.004C277.097 408.769 294.564 331.846 283.824 244.374C273.084 156.903 238.204 92.0061 187.427 98.2407C136.65 104.475 104.194 180.439 114.934 267.911C125.674 355.383 175.544 421.238 226.321 415.004Z",
|
|
2577
|
+
fill: "white",
|
|
2578
|
+
stroke: "white",
|
|
2579
|
+
strokeWidth: "10"
|
|
2580
|
+
}
|
|
2581
|
+
), /* @__PURE__ */ React2.createElement(
|
|
2582
|
+
"path",
|
|
2583
|
+
{
|
|
2584
|
+
d: "M182.564 395.999C201.42 431.462 270.873 431.411 337.69 395.883C404.508 360.356 443.388 302.806 424.531 267.342C405.675 231.879 336.223 231.931 269.405 267.458C202.588 302.986 163.708 370.535 182.564 395.999Z",
|
|
2585
|
+
fill: "white",
|
|
2586
|
+
stroke: "white",
|
|
2587
|
+
strokeWidth: "10"
|
|
2588
|
+
}
|
|
2589
|
+
))
|
|
2590
|
+
),
|
|
2591
|
+
/* @__PURE__ */ React2.createElement("span", null, branding.label || "Localized by", " ", /* @__PURE__ */ React2.createElement("strong", { "data-no-translate": true, style: { color: tokens.text } }, "Lovalingo"))
|
|
2592
|
+
)))));
|
|
2593
|
+
};
|
|
2594
|
+
|
|
2595
|
+
// src/components/provider/editModeUtils.ts
|
|
2596
|
+
function readEditParams() {
|
|
2597
|
+
if (typeof window === "undefined") return { enabled: false, editKey: null };
|
|
2598
|
+
const params = new URLSearchParams(window.location.search);
|
|
2599
|
+
const rawFlag = (params.get(EDIT_MODE_PARAM) || params.get("editMode") || "").trim().toLowerCase();
|
|
2600
|
+
const enabled = EDIT_MODE_VALUES.has(rawFlag);
|
|
2601
|
+
const editKey = (params.get(EDIT_KEY_PARAM) || params.get("editKey") || "").trim() || null;
|
|
2602
|
+
return { enabled, editKey };
|
|
2603
|
+
}
|
|
2604
|
+
function cssEscape(value) {
|
|
2605
|
+
const esc = typeof window !== "undefined" && window?.CSS?.escape;
|
|
2606
|
+
if (typeof esc === "function") return esc(value);
|
|
2607
|
+
return value.replace(/[ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, "\\$&");
|
|
2608
|
+
}
|
|
2609
|
+
function buildCssSelector(element, maxDepth = 5) {
|
|
2610
|
+
if (!element || !element.tagName) return null;
|
|
2611
|
+
if (element.id) return `#${cssEscape(element.id)}`;
|
|
2612
|
+
const parts = [];
|
|
2613
|
+
let node = element;
|
|
2614
|
+
let depth = 0;
|
|
2615
|
+
while (node && depth < maxDepth) {
|
|
2616
|
+
const tag = node.tagName.toLowerCase();
|
|
2617
|
+
if (!tag || tag === "html") break;
|
|
2618
|
+
let part = tag;
|
|
2619
|
+
const nodeTag = node.tagName;
|
|
2620
|
+
const classes = Array.from(node.classList || []).filter(Boolean).filter((cls) => !cls.startsWith("lovalingo-")).slice(0, 2);
|
|
2621
|
+
if (classes.length > 0) {
|
|
2622
|
+
part += `.${classes.map(cssEscape).join(".")}`;
|
|
2623
|
+
}
|
|
2624
|
+
const parentEl = node.parentElement;
|
|
2625
|
+
if (parentEl) {
|
|
2626
|
+
const siblings = Array.from(parentEl.children).filter(
|
|
2627
|
+
(child) => child instanceof HTMLElement && child.tagName === nodeTag
|
|
2628
|
+
);
|
|
2629
|
+
if (siblings.length > 1) {
|
|
2630
|
+
part += `:nth-of-type(${siblings.indexOf(node) + 1})`;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
parts.unshift(part);
|
|
2634
|
+
const selector = parts.join(" > ");
|
|
2635
|
+
try {
|
|
2636
|
+
if (document.querySelectorAll(selector).length === 1) return selector;
|
|
2637
|
+
} catch {
|
|
2638
|
+
}
|
|
2639
|
+
node = parentEl;
|
|
2640
|
+
depth += 1;
|
|
2641
|
+
}
|
|
2642
|
+
return parts.join(" > ") || null;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
// src/components/provider/seoUtils.ts
|
|
2646
|
+
function applySeoBundle(bundle, hreflangEnabled) {
|
|
2647
|
+
try {
|
|
2648
|
+
const head = document.head;
|
|
2649
|
+
if (!head) return;
|
|
2650
|
+
if (!bundle) return;
|
|
2651
|
+
const seo = bundle?.seo && typeof bundle.seo === "object" ? bundle.seo : {};
|
|
2652
|
+
const alternates = bundle?.alternates && typeof bundle.alternates === "object" ? bundle.alternates : {};
|
|
2653
|
+
const setOrCreateMeta = (attrs, content) => {
|
|
2654
|
+
const key = attrs.name ? `meta[name="${attrs.name}"]` : attrs.property ? `meta[property="${attrs.property}"]` : "";
|
|
2655
|
+
const selector = key || "meta";
|
|
2656
|
+
const existing = selector ? head.querySelector(selector) : null;
|
|
2657
|
+
const el = existing || document.createElement("meta");
|
|
2658
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
2659
|
+
el.setAttribute(k, v);
|
|
2660
|
+
}
|
|
2661
|
+
el.setAttribute("content", content);
|
|
2662
|
+
if (!existing) head.appendChild(el);
|
|
2663
|
+
};
|
|
2664
|
+
const setOrCreateTitle = (value) => {
|
|
2665
|
+
const existing = head.querySelector("title");
|
|
2666
|
+
if (existing) {
|
|
2667
|
+
existing.textContent = value;
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
const el = document.createElement("title");
|
|
2671
|
+
el.textContent = value;
|
|
2672
|
+
head.appendChild(el);
|
|
2673
|
+
};
|
|
2674
|
+
const getString = (value) => typeof value === "string" && value.trim() ? value.trim() : "";
|
|
2675
|
+
const title = getString(seo.title);
|
|
2676
|
+
if (title) setOrCreateTitle(title);
|
|
2677
|
+
const description = getString(seo.description);
|
|
2678
|
+
if (description) setOrCreateMeta({ name: "description" }, description);
|
|
2679
|
+
const robots = getString(seo.robots);
|
|
2680
|
+
if (robots) setOrCreateMeta({ name: "robots" }, robots);
|
|
2681
|
+
const ogTitle = getString(seo.og_title);
|
|
2682
|
+
if (ogTitle) setOrCreateMeta({ property: "og:title" }, ogTitle);
|
|
2683
|
+
const ogDescription = getString(seo.og_description);
|
|
2684
|
+
if (ogDescription) setOrCreateMeta({ property: "og:description" }, ogDescription);
|
|
2685
|
+
const ogImage = getString(seo.og_image);
|
|
2686
|
+
if (ogImage) setOrCreateMeta({ property: "og:image" }, ogImage);
|
|
2687
|
+
const ogImageAlt = getString(seo.og_image_alt);
|
|
2688
|
+
if (ogImageAlt) setOrCreateMeta({ property: "og:image:alt" }, ogImageAlt);
|
|
2689
|
+
const twitterCard = getString(seo.twitter_card);
|
|
2690
|
+
if (twitterCard) setOrCreateMeta({ name: "twitter:card" }, twitterCard);
|
|
2691
|
+
const twitterTitle = getString(seo.twitter_title);
|
|
2692
|
+
if (twitterTitle) setOrCreateMeta({ name: "twitter:title" }, twitterTitle);
|
|
2693
|
+
const twitterDescription = getString(seo.twitter_description);
|
|
2694
|
+
if (twitterDescription) setOrCreateMeta({ name: "twitter:description" }, twitterDescription);
|
|
2695
|
+
const twitterImage = getString(seo.twitter_image);
|
|
2696
|
+
if (twitterImage) setOrCreateMeta({ name: "twitter:image" }, twitterImage);
|
|
2697
|
+
const twitterImageAlt = getString(seo.twitter_image_alt);
|
|
2698
|
+
if (twitterImageAlt) setOrCreateMeta({ name: "twitter:image:alt" }, twitterImageAlt);
|
|
2699
|
+
const canonicalHref = resolveCanonicalHref(seo, alternates);
|
|
2700
|
+
const languages = alternates.languages && typeof alternates.languages === "object" ? alternates.languages : {};
|
|
2701
|
+
const hasAnyAlternate = Boolean(alternates.xDefault) || Object.values(languages).some(Boolean);
|
|
2702
|
+
if (!canonicalHref && !(hreflangEnabled && hasAnyAlternate)) return;
|
|
2703
|
+
head.querySelectorAll('link[rel="canonical"], link[rel="alternate"][hreflang], link[data-Lovalingo="hreflang"], link[data-Lovalingo="canonical"]').forEach((el) => el.remove());
|
|
2704
|
+
if (canonicalHref) {
|
|
2705
|
+
const canonical = document.createElement("link");
|
|
2706
|
+
canonical.rel = "canonical";
|
|
2707
|
+
canonical.href = canonicalHref;
|
|
2708
|
+
canonical.setAttribute("data-Lovalingo", "canonical");
|
|
2709
|
+
head.appendChild(canonical);
|
|
2710
|
+
}
|
|
2711
|
+
if (!hreflangEnabled) return;
|
|
2712
|
+
for (const [lang, href] of Object.entries(languages)) {
|
|
2713
|
+
if (!href) continue;
|
|
2714
|
+
const link = document.createElement("link");
|
|
2715
|
+
link.rel = "alternate";
|
|
2716
|
+
link.hreflang = lang;
|
|
2717
|
+
link.href = href;
|
|
2718
|
+
link.setAttribute("data-Lovalingo", "hreflang");
|
|
2719
|
+
head.appendChild(link);
|
|
2720
|
+
}
|
|
2721
|
+
if (alternates.xDefault) {
|
|
2722
|
+
const xDefault = document.createElement("link");
|
|
2723
|
+
xDefault.rel = "alternate";
|
|
2724
|
+
xDefault.hreflang = "x-default";
|
|
2725
|
+
xDefault.href = alternates.xDefault;
|
|
2726
|
+
xDefault.setAttribute("data-Lovalingo", "hreflang");
|
|
2727
|
+
head.appendChild(xDefault);
|
|
2728
|
+
}
|
|
2729
|
+
} catch {
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
function resolveCanonicalHref(seo, alternates) {
|
|
2733
|
+
const alt = alternates && typeof alternates === "object" ? alternates : null;
|
|
2734
|
+
if (alt && typeof alt.canonical === "string" && alt.canonical.trim()) return alt.canonical.trim();
|
|
2735
|
+
if (typeof seo?.canonical_url === "string" && seo.canonical_url.trim()) return seo.canonical_url.trim();
|
|
2736
|
+
return "";
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
// src/components/provider/useEditModeOverlay.ts
|
|
2740
|
+
import { useEffect as useEffect9, useRef as useRef7 } from "react";
|
|
2741
|
+
function useEditModeOverlay({ editMode, excludeElement, setEditMode }) {
|
|
2742
|
+
const editSavingRef = useRef7(false);
|
|
2743
|
+
useEffect9(() => {
|
|
2744
|
+
if (typeof window === "undefined") return;
|
|
2745
|
+
const existingHighlight = document.getElementById(EDIT_HIGHLIGHT_ID);
|
|
2746
|
+
const existingHint = document.getElementById(EDIT_HINT_ID);
|
|
2747
|
+
if (!editMode) {
|
|
2748
|
+
existingHighlight?.remove();
|
|
2749
|
+
existingHint?.remove();
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2752
|
+
const highlight = existingHighlight || (() => {
|
|
2753
|
+
const node = document.createElement("div");
|
|
2754
|
+
node.id = EDIT_HIGHLIGHT_ID;
|
|
2755
|
+
node.setAttribute(EDIT_UI_ATTR, "true");
|
|
2756
|
+
node.setAttribute("data-lovalingo-exclude", "true");
|
|
2757
|
+
node.style.position = "fixed";
|
|
2758
|
+
node.style.pointerEvents = "none";
|
|
2759
|
+
node.style.zIndex = "2147483646";
|
|
2760
|
+
node.style.border = "2px solid #22c55e";
|
|
2761
|
+
node.style.background = "rgba(34, 197, 94, 0.12)";
|
|
2762
|
+
node.style.borderRadius = "8px";
|
|
2763
|
+
node.style.boxSizing = "border-box";
|
|
2764
|
+
node.style.transition = "transform 80ms ease, width 80ms ease, height 80ms ease";
|
|
2765
|
+
node.style.display = "none";
|
|
2766
|
+
document.body.appendChild(node);
|
|
2767
|
+
return node;
|
|
2768
|
+
})();
|
|
2769
|
+
const hint = existingHint || (() => {
|
|
2770
|
+
const node = document.createElement("div");
|
|
2771
|
+
node.id = EDIT_HINT_ID;
|
|
2772
|
+
node.setAttribute(EDIT_UI_ATTR, "true");
|
|
2773
|
+
node.setAttribute("data-lovalingo-exclude", "true");
|
|
2774
|
+
node.style.position = "fixed";
|
|
2775
|
+
node.style.left = "12px";
|
|
2776
|
+
node.style.bottom = "12px";
|
|
2777
|
+
node.style.zIndex = "2147483647";
|
|
2778
|
+
node.style.background = "rgba(10, 10, 10, 0.85)";
|
|
2779
|
+
node.style.color = "#ffffff";
|
|
2780
|
+
node.style.fontSize = "12px";
|
|
2781
|
+
node.style.lineHeight = "1.4";
|
|
2782
|
+
node.style.padding = "8px 10px";
|
|
2783
|
+
node.style.borderRadius = "8px";
|
|
2784
|
+
node.style.border = "1px solid rgba(255, 255, 255, 0.15)";
|
|
2785
|
+
node.style.pointerEvents = "none";
|
|
2786
|
+
node.style.maxWidth = "280px";
|
|
2787
|
+
node.textContent = "Edit Mode: click an element to exclude. Press Esc to exit.";
|
|
2788
|
+
document.body.appendChild(node);
|
|
2789
|
+
return node;
|
|
2790
|
+
})();
|
|
2791
|
+
let rafId = null;
|
|
2792
|
+
let pendingTarget = null;
|
|
2793
|
+
const previousCursor = document.body.style.cursor;
|
|
2794
|
+
document.body.style.cursor = "crosshair";
|
|
2795
|
+
const updateHighlight = () => {
|
|
2796
|
+
rafId = null;
|
|
2797
|
+
if (!pendingTarget) {
|
|
2798
|
+
highlight.style.display = "none";
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
const rect = pendingTarget.getBoundingClientRect();
|
|
2802
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
2803
|
+
highlight.style.display = "none";
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
highlight.style.display = "block";
|
|
2807
|
+
highlight.style.width = `${rect.width}px`;
|
|
2808
|
+
highlight.style.height = `${rect.height}px`;
|
|
2809
|
+
highlight.style.transform = `translate(${rect.left}px, ${rect.top}px)`;
|
|
2810
|
+
};
|
|
2811
|
+
const onMove = (event) => {
|
|
2812
|
+
const rawTarget = event.target;
|
|
2813
|
+
const target = rawTarget instanceof HTMLElement ? rawTarget : rawTarget instanceof Node ? rawTarget.parentElement : null;
|
|
2814
|
+
if (!target || target.closest(`[${EDIT_UI_ATTR}="true"]`)) {
|
|
2815
|
+
pendingTarget = null;
|
|
2816
|
+
} else if (target === document.body || target === document.documentElement) {
|
|
2817
|
+
pendingTarget = null;
|
|
2818
|
+
} else {
|
|
2819
|
+
pendingTarget = target;
|
|
2820
|
+
}
|
|
2821
|
+
if (rafId !== null) return;
|
|
2822
|
+
rafId = window.requestAnimationFrame(updateHighlight);
|
|
2823
|
+
};
|
|
2824
|
+
const onClick = async (event) => {
|
|
2825
|
+
const rawTarget = event.target;
|
|
2826
|
+
const target = rawTarget instanceof HTMLElement ? rawTarget : rawTarget instanceof Node ? rawTarget.parentElement : null;
|
|
2827
|
+
if (!target || target.closest(`[${EDIT_UI_ATTR}="true"]`)) return;
|
|
2828
|
+
event.preventDefault();
|
|
2829
|
+
event.stopPropagation();
|
|
2830
|
+
event.stopImmediatePropagation();
|
|
2831
|
+
const selector = buildCssSelector(target);
|
|
2832
|
+
if (!selector) return;
|
|
2833
|
+
if (editSavingRef.current) return;
|
|
2834
|
+
editSavingRef.current = true;
|
|
2835
|
+
try {
|
|
2836
|
+
await excludeElement(selector);
|
|
2837
|
+
} finally {
|
|
2838
|
+
editSavingRef.current = false;
|
|
2839
|
+
}
|
|
2840
|
+
};
|
|
2841
|
+
const onKeyDown = (event) => {
|
|
2842
|
+
if (event.key === "Escape") {
|
|
2843
|
+
event.preventDefault();
|
|
2844
|
+
setEditMode(false);
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
document.addEventListener("mousemove", onMove, true);
|
|
2848
|
+
document.addEventListener("click", onClick, true);
|
|
2849
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
2850
|
+
return () => {
|
|
2851
|
+
document.removeEventListener("mousemove", onMove, true);
|
|
2852
|
+
document.removeEventListener("click", onClick, true);
|
|
2853
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
2854
|
+
if (rafId !== null) window.cancelAnimationFrame(rafId);
|
|
2855
|
+
highlight.remove();
|
|
2856
|
+
hint.remove();
|
|
2857
|
+
document.body.style.cursor = previousCursor;
|
|
2858
|
+
};
|
|
2859
|
+
}, [editMode, excludeElement, setEditMode]);
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
// src/components/provider/useHistoryNavigationPatch.ts
|
|
2863
|
+
import { useEffect as useEffect10, useRef as useRef8 } from "react";
|
|
2864
|
+
function useHistoryNavigationPatch(onNavigateRef) {
|
|
2865
|
+
const historyPatchedRef = useRef8(false);
|
|
2866
|
+
const originalHistoryRef = useRef8(null);
|
|
2867
|
+
useEffect10(() => {
|
|
2868
|
+
if (typeof window === "undefined") return;
|
|
2869
|
+
if (historyPatchedRef.current) return;
|
|
2870
|
+
historyPatchedRef.current = true;
|
|
2871
|
+
const historyObj = window.history;
|
|
2872
|
+
const originalPushState = historyObj.pushState.bind(historyObj);
|
|
2873
|
+
const originalReplaceState = historyObj.replaceState.bind(historyObj);
|
|
2874
|
+
originalHistoryRef.current = { pushState: originalPushState, replaceState: originalReplaceState };
|
|
2875
|
+
const safeOnNavigate = () => {
|
|
2876
|
+
try {
|
|
2877
|
+
onNavigateRef.current();
|
|
2878
|
+
} catch {
|
|
2879
|
+
}
|
|
2880
|
+
};
|
|
2881
|
+
historyObj.pushState = ((...args) => {
|
|
2882
|
+
const ret = originalPushState(...args);
|
|
2883
|
+
safeOnNavigate();
|
|
2884
|
+
return ret;
|
|
2885
|
+
});
|
|
2886
|
+
historyObj.replaceState = ((...args) => {
|
|
2887
|
+
const ret = originalReplaceState(...args);
|
|
2888
|
+
safeOnNavigate();
|
|
2889
|
+
return ret;
|
|
2890
|
+
});
|
|
2891
|
+
window.addEventListener("popstate", safeOnNavigate);
|
|
2892
|
+
window.addEventListener("hashchange", safeOnNavigate);
|
|
2893
|
+
return () => {
|
|
2894
|
+
const originals = originalHistoryRef.current;
|
|
2895
|
+
if (originals) {
|
|
2896
|
+
historyObj.pushState = originals.pushState;
|
|
2897
|
+
historyObj.replaceState = originals.replaceState;
|
|
2898
|
+
}
|
|
2899
|
+
window.removeEventListener("popstate", safeOnNavigate);
|
|
2900
|
+
window.removeEventListener("hashchange", safeOnNavigate);
|
|
2901
|
+
originalHistoryRef.current = null;
|
|
2902
|
+
historyPatchedRef.current = false;
|
|
2903
|
+
};
|
|
2904
|
+
}, [onNavigateRef]);
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
// src/components/provider/localeUtils.ts
|
|
2908
|
+
function detectLocaleFromLocation({ routing, allLocales, defaultLocale }) {
|
|
2909
|
+
if (routing === "path") {
|
|
2910
|
+
const pathLocale = window.location.pathname.split("/")[1];
|
|
2911
|
+
if (pathLocale && allLocales.includes(pathLocale)) {
|
|
2912
|
+
return pathLocale;
|
|
2913
|
+
}
|
|
2914
|
+
} else if (routing === "query") {
|
|
2915
|
+
const params = new URLSearchParams(window.location.search);
|
|
2916
|
+
const queryLocale = params.get("t") || params.get("locale");
|
|
2917
|
+
if (queryLocale && allLocales.includes(queryLocale)) {
|
|
2918
|
+
return queryLocale;
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
try {
|
|
2922
|
+
const storedLocale = localStorage.getItem(LOCALE_STORAGE_KEY);
|
|
2923
|
+
if (storedLocale && allLocales.includes(storedLocale)) {
|
|
2924
|
+
return storedLocale;
|
|
2925
|
+
}
|
|
2926
|
+
} catch (e) {
|
|
2927
|
+
warnDebug("localStorage not available:", e);
|
|
2928
|
+
}
|
|
2929
|
+
return defaultLocale;
|
|
2930
|
+
}
|
|
2931
|
+
function setDocumentLocale(nextLocale) {
|
|
2932
|
+
try {
|
|
2933
|
+
const html = document.documentElement;
|
|
2934
|
+
if (!html) return;
|
|
2935
|
+
html.setAttribute("lang", nextLocale);
|
|
2936
|
+
const rtlLocales = /* @__PURE__ */ new Set(["ar", "he", "fa", "ur"]);
|
|
2937
|
+
html.setAttribute("dir", rtlLocales.has(nextLocale) ? "rtl" : "ltr");
|
|
2938
|
+
} catch {
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
// src/components/provider/useProviderCache.ts
|
|
2943
|
+
import { useCallback as useCallback6, useEffect as useEffect11, useState as useState3 } from "react";
|
|
2944
|
+
function useProviderCache({ overlayBgColor, resolvedApiKey }) {
|
|
2945
|
+
const loadingBgStorageKey = `${LOADING_BG_STORAGE_PREFIX}:${resolvedApiKey || "anonymous"}`;
|
|
2946
|
+
const brandingStorageKey = `${BRANDING_STORAGE_PREFIX}:${resolvedApiKey || "anonymous"}`;
|
|
2947
|
+
const readBrandingCache = useCallback6(() => {
|
|
2948
|
+
try {
|
|
2949
|
+
const cached = (localStorage.getItem(brandingStorageKey) || "").trim();
|
|
2950
|
+
if (cached === "0") return false;
|
|
2951
|
+
if (cached === "1") return true;
|
|
2952
|
+
} catch {
|
|
2953
|
+
}
|
|
2954
|
+
return true;
|
|
2955
|
+
}, [brandingStorageKey]);
|
|
2956
|
+
const [brandingEnabled, setBrandingEnabled] = useState3(readBrandingCache);
|
|
2957
|
+
const getCachedLoadingBgColor = useCallback6(() => {
|
|
2958
|
+
const configured = (overlayBgColor || "").toString().trim();
|
|
2959
|
+
if (/^#[0-9a-fA-F]{6}$/.test(configured)) return configured;
|
|
2960
|
+
try {
|
|
2961
|
+
const cached = localStorage.getItem(loadingBgStorageKey) || "";
|
|
2962
|
+
if (/^#[0-9a-fA-F]{6}$/.test(cached.trim())) return cached.trim();
|
|
2963
|
+
} catch {
|
|
2964
|
+
}
|
|
2965
|
+
try {
|
|
2966
|
+
const bodyBg = window.getComputedStyle(document.body).backgroundColor;
|
|
2967
|
+
if (bodyBg && bodyBg !== "transparent" && bodyBg !== "rgba(0, 0, 0, 0)") return bodyBg;
|
|
2968
|
+
const htmlBg = window.getComputedStyle(document.documentElement).backgroundColor;
|
|
2969
|
+
if (htmlBg && htmlBg !== "transparent" && htmlBg !== "rgba(0, 0, 0, 0)") return htmlBg;
|
|
2970
|
+
} catch {
|
|
2971
|
+
}
|
|
2972
|
+
return "#ffffff";
|
|
2973
|
+
}, [loadingBgStorageKey, overlayBgColor]);
|
|
2974
|
+
const setCachedLoadingBgColor = useCallback6(
|
|
2975
|
+
(color) => {
|
|
2976
|
+
const next = (color || "").toString().trim();
|
|
2977
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(next)) return;
|
|
2978
|
+
try {
|
|
2979
|
+
localStorage.setItem(loadingBgStorageKey, next);
|
|
2980
|
+
} catch {
|
|
2981
|
+
}
|
|
2982
|
+
},
|
|
2983
|
+
[loadingBgStorageKey]
|
|
2984
|
+
);
|
|
2985
|
+
useEffect11(() => {
|
|
2986
|
+
const configured = (overlayBgColor || "").toString().trim();
|
|
2987
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(configured)) return;
|
|
2988
|
+
setCachedLoadingBgColor(configured);
|
|
2989
|
+
}, [overlayBgColor, setCachedLoadingBgColor]);
|
|
2990
|
+
const setCachedBrandingEnabled = useCallback6(
|
|
2991
|
+
(enabled) => {
|
|
2992
|
+
try {
|
|
2993
|
+
localStorage.setItem(brandingStorageKey, enabled === false ? "0" : "1");
|
|
2994
|
+
} catch {
|
|
2995
|
+
}
|
|
2996
|
+
},
|
|
2997
|
+
[brandingStorageKey]
|
|
2998
|
+
);
|
|
2999
|
+
useEffect11(() => {
|
|
3000
|
+
setBrandingEnabled(readBrandingCache());
|
|
3001
|
+
}, [readBrandingCache]);
|
|
3002
|
+
return {
|
|
3003
|
+
brandingEnabled,
|
|
3004
|
+
setBrandingEnabled,
|
|
3005
|
+
setCachedBrandingEnabled,
|
|
3006
|
+
getCachedLoadingBgColor,
|
|
3007
|
+
setCachedLoadingBgColor
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
// src/components/LovalingoProvider.tsx
|
|
3012
|
+
var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect12;
|
|
3013
|
+
var LovalingoProvider = ({
|
|
3014
|
+
children,
|
|
3015
|
+
apiKey: apiKeyProp,
|
|
3016
|
+
publicAnonKey,
|
|
3017
|
+
defaultLocale,
|
|
3018
|
+
locales,
|
|
3019
|
+
apiBase = "https://cdn.lovalingo.com",
|
|
3020
|
+
routing = "path",
|
|
3021
|
+
// Default to path mode (SEO-friendly, recommended)
|
|
3022
|
+
autoPrefixLinks = true,
|
|
3023
|
+
overlayBgColor,
|
|
3024
|
+
autoApplyRules = true,
|
|
3025
|
+
switcherPosition = "bottom-right",
|
|
3026
|
+
switcherOffsetY = 20,
|
|
3027
|
+
switcherTheme = "dark",
|
|
3028
|
+
editMode: initialEditMode = false,
|
|
3029
|
+
editKey = "KeyE",
|
|
3030
|
+
pathNormalization = DEFAULT_PATH_NORMALIZATION,
|
|
3031
|
+
// Enable by default
|
|
3032
|
+
mode = "dom",
|
|
3033
|
+
// Default to legacy DOM mode for backward compatibility
|
|
3034
|
+
sitemap = true,
|
|
3035
|
+
// Default: true - Auto-inject sitemap link tag
|
|
3036
|
+
seo = true,
|
|
3037
|
+
// Default: true - Can be disabled per project entitlements
|
|
3038
|
+
navigateRef
|
|
3039
|
+
// For path mode routing
|
|
3040
|
+
}) => {
|
|
3041
|
+
const metaKey = typeof document !== "undefined" ? document.querySelector('meta[name="lovalingo-public-anon-key"]')?.content?.trim() || "" : "";
|
|
3042
|
+
const resolvedApiKey = typeof apiKeyProp === "string" && apiKeyProp.trim().length > 0 ? apiKeyProp : typeof publicAnonKey === "string" && publicAnonKey.trim().length > 0 ? publicAnonKey : globalThis.__LOVALINGO_PUBLIC_ANON_KEY__ || globalThis.__LOVALINGO_API_KEY__ || metaKey || "";
|
|
3043
|
+
const rawLocales = Array.isArray(locales) ? locales : [];
|
|
3044
|
+
const localesKey = rawLocales.join(",");
|
|
3045
|
+
const allLocales = useMemo2(() => {
|
|
3046
|
+
const base = rawLocales.includes(defaultLocale) ? rawLocales : [defaultLocale, ...rawLocales];
|
|
3047
|
+
return Array.from(new Set(base));
|
|
3048
|
+
}, [defaultLocale, localesKey]);
|
|
3049
|
+
const pathNormalizationKey = (() => {
|
|
3050
|
+
const enabled = pathNormalization?.enabled !== false;
|
|
3051
|
+
const rules = Array.isArray(pathNormalization?.rules) ? pathNormalization.rules : [];
|
|
3052
|
+
const rulesKey = rules.map((r) => `${r.pattern}=>${r.replacement}:${r.includeSubpaths ? 1 : 0}`).join("|");
|
|
3053
|
+
return `${enabled ? 1 : 0}:${rulesKey}`;
|
|
3054
|
+
})();
|
|
3055
|
+
const stablePathNormalization = useMemo2(() => {
|
|
3056
|
+
const enabled = pathNormalization?.enabled !== false;
|
|
3057
|
+
const rules = Array.isArray(pathNormalization?.rules) ? pathNormalization.rules : void 0;
|
|
3058
|
+
return rules ? { enabled, rules } : { enabled };
|
|
3059
|
+
}, [pathNormalizationKey]);
|
|
3060
|
+
const [locale, setLocaleState] = useState4(() => {
|
|
3061
|
+
if (typeof window === "undefined") return defaultLocale;
|
|
3062
|
+
if (routing === "path") {
|
|
3063
|
+
const pathLocale = window.location.pathname.split("/")[1];
|
|
3064
|
+
if (pathLocale && allLocales.includes(pathLocale)) {
|
|
3065
|
+
return pathLocale;
|
|
3066
|
+
}
|
|
3067
|
+
} else if (routing === "query") {
|
|
3068
|
+
const params = new URLSearchParams(window.location.search);
|
|
3069
|
+
const queryLocale = params.get("t") || params.get("locale");
|
|
3070
|
+
if (queryLocale && allLocales.includes(queryLocale)) {
|
|
3071
|
+
return queryLocale;
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
try {
|
|
3075
|
+
const stored = localStorage.getItem(LOCALE_STORAGE_KEY);
|
|
3076
|
+
if (stored && allLocales.includes(stored)) {
|
|
3077
|
+
return stored;
|
|
3078
|
+
}
|
|
3079
|
+
} catch {
|
|
3080
|
+
}
|
|
3081
|
+
return defaultLocale;
|
|
3082
|
+
});
|
|
3083
|
+
const initialEditParams = readEditParams();
|
|
3084
|
+
const [editMode, setEditMode] = useState4(initialEditMode || initialEditParams.enabled);
|
|
3085
|
+
const [editSecretKey] = useState4(initialEditParams.editKey);
|
|
3086
|
+
const enhancedPathConfig = useMemo2(
|
|
3087
|
+
() => routing === "path" ? { ...stablePathNormalization, supportedLocales: allLocales } : stablePathNormalization,
|
|
3088
|
+
[allLocales, routing, stablePathNormalization]
|
|
3089
|
+
);
|
|
3090
|
+
const apiRef = useRef9(new LovalingoAPI(resolvedApiKey, apiBase, enhancedPathConfig, editSecretKey ?? void 0));
|
|
3091
|
+
useEffect12(() => {
|
|
3092
|
+
apiRef.current = new LovalingoAPI(resolvedApiKey, apiBase, enhancedPathConfig, editSecretKey ?? void 0);
|
|
3093
|
+
}, [apiBase, editSecretKey, enhancedPathConfig, resolvedApiKey]);
|
|
3094
|
+
const routingConfig = useContext(LangRoutingContext);
|
|
3095
|
+
const [entitlements, setEntitlements] = useState4(() => apiRef.current.getEntitlements());
|
|
3096
|
+
const { trackPageviewOnce } = usePageviewTracking({ apiRef, resolvedApiKey });
|
|
3097
|
+
const lastNormalizedPathRef = useRef9("");
|
|
3098
|
+
const onNavigateRef = useRef9(() => void 0);
|
|
3099
|
+
const isInternalNavigationRef = useRef9(false);
|
|
3100
|
+
const {
|
|
3101
|
+
brandingEnabled,
|
|
3102
|
+
setBrandingEnabled,
|
|
3103
|
+
setCachedBrandingEnabled,
|
|
3104
|
+
getCachedLoadingBgColor,
|
|
3105
|
+
setCachedLoadingBgColor
|
|
3106
|
+
} = useProviderCache({ overlayBgColor, resolvedApiKey });
|
|
3107
|
+
const config = {
|
|
3108
|
+
apiKey: resolvedApiKey,
|
|
3109
|
+
publicAnonKey: resolvedApiKey,
|
|
3110
|
+
defaultLocale,
|
|
3111
|
+
locales: allLocales,
|
|
3112
|
+
apiBase,
|
|
3113
|
+
routing,
|
|
3114
|
+
autoPrefixLinks,
|
|
3115
|
+
overlayBgColor,
|
|
3116
|
+
switcherPosition,
|
|
3117
|
+
switcherOffsetY,
|
|
3118
|
+
switcherTheme,
|
|
3119
|
+
editMode: initialEditMode,
|
|
3120
|
+
editKey,
|
|
3121
|
+
pathNormalization,
|
|
3122
|
+
mode,
|
|
3123
|
+
autoApplyRules
|
|
3124
|
+
};
|
|
3125
|
+
const isSeoActive = useCallback7(() => {
|
|
3126
|
+
const serverEnabled = entitlements?.seoEnabled;
|
|
3127
|
+
if (serverEnabled === false) return false;
|
|
3128
|
+
return seo !== false;
|
|
3129
|
+
}, [entitlements, seo]);
|
|
3130
|
+
useEffect12(() => {
|
|
3131
|
+
const stop = startMarkerEngine({ throttleMs: 120 });
|
|
3132
|
+
return () => stop();
|
|
3133
|
+
}, []);
|
|
3134
|
+
const detectLocale = useCallback7(
|
|
3135
|
+
() => detectLocaleFromLocation({ routing, allLocales, defaultLocale }),
|
|
3136
|
+
[allLocales, defaultLocale, routing]
|
|
3137
|
+
);
|
|
3138
|
+
useEffect12(() => {
|
|
3139
|
+
if (locale !== defaultLocale) return;
|
|
3140
|
+
if (entitlements) return;
|
|
3141
|
+
let cancelled = false;
|
|
3142
|
+
(async () => {
|
|
3143
|
+
const bootstrap = await apiRef.current.fetchBootstrap(locale, window.location.pathname + window.location.search);
|
|
3144
|
+
if (cancelled) return;
|
|
3145
|
+
if (bootstrap?.entitlements) {
|
|
3146
|
+
setEntitlements(mergeEntitlementsSeoEnabled(bootstrap.entitlements, bootstrap.seoEnabled));
|
|
3147
|
+
}
|
|
3148
|
+
if (bootstrap?.loading_bg_color) setCachedLoadingBgColor(bootstrap.loading_bg_color);
|
|
3149
|
+
if (bootstrap?.entitlements?.brandingRequired) {
|
|
3150
|
+
setBrandingEnabled(true);
|
|
3151
|
+
setCachedBrandingEnabled(true);
|
|
3152
|
+
} else if (typeof bootstrap?.branding_enabled === "boolean") {
|
|
3153
|
+
setBrandingEnabled(bootstrap.branding_enabled);
|
|
3154
|
+
setCachedBrandingEnabled(bootstrap.branding_enabled);
|
|
3155
|
+
}
|
|
3156
|
+
})();
|
|
3157
|
+
return () => {
|
|
3158
|
+
cancelled = true;
|
|
3159
|
+
};
|
|
3160
|
+
}, [defaultLocale, entitlements, locale, setCachedBrandingEnabled, setCachedLoadingBgColor]);
|
|
3161
|
+
const applySeoBundle2 = useCallback7(
|
|
3162
|
+
(bundle, hreflangEnabled) => {
|
|
3163
|
+
applySeoBundle(bundle, hreflangEnabled);
|
|
3164
|
+
},
|
|
3165
|
+
[]
|
|
3166
|
+
);
|
|
3167
|
+
useEffect12(() => {
|
|
3168
|
+
setDocumentLocale(locale);
|
|
3169
|
+
if (locale !== defaultLocale) return;
|
|
3170
|
+
if (!isSeoActive()) return;
|
|
3171
|
+
void apiRef.current.fetchSeoBundle(locale).then((bundle) => {
|
|
3172
|
+
applySeoBundle2(bundle, Boolean(entitlements?.hreflangEnabled));
|
|
3173
|
+
});
|
|
3174
|
+
}, [applySeoBundle2, defaultLocale, entitlements, isSeoActive, locale, setDocumentLocale]);
|
|
3175
|
+
const { isLoading, isNavigatingRef, loadData } = useBundleLoading({
|
|
3176
|
+
apiRef,
|
|
3177
|
+
resolvedApiKey,
|
|
3178
|
+
defaultLocale,
|
|
3179
|
+
routing,
|
|
3180
|
+
allLocales,
|
|
3181
|
+
nonLocalizedPaths: routingConfig.nonLocalizedPaths,
|
|
3182
|
+
enhancedPathConfig,
|
|
3183
|
+
mode,
|
|
3184
|
+
autoApplyRules,
|
|
3185
|
+
seoProp: seo,
|
|
3186
|
+
isSeoActive,
|
|
3187
|
+
applySeoBundle: applySeoBundle2,
|
|
3188
|
+
setEntitlements,
|
|
3189
|
+
setBrandingEnabled,
|
|
3190
|
+
setCachedBrandingEnabled,
|
|
3191
|
+
setCachedLoadingBgColor,
|
|
3192
|
+
getCachedLoadingBgColor
|
|
3193
|
+
});
|
|
3194
|
+
useEffect12(() => {
|
|
3195
|
+
onNavigateRef.current = () => {
|
|
3196
|
+
trackPageviewOnce(window.location.pathname + window.location.search);
|
|
3197
|
+
const nextLocale = detectLocale();
|
|
3198
|
+
const normalizedPath = processPath(window.location.pathname, enhancedPathConfig);
|
|
3199
|
+
const normalizedPathChanged = normalizedPath !== lastNormalizedPathRef.current;
|
|
3200
|
+
lastNormalizedPathRef.current = normalizedPath;
|
|
3201
|
+
if (normalizedPathChanged && nextLocale !== defaultLocale && !isInternalNavigationRef.current) {
|
|
3202
|
+
void loadData(nextLocale, locale);
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
if (nextLocale !== locale) {
|
|
3206
|
+
setLocaleState(nextLocale);
|
|
3207
|
+
if (!isInternalNavigationRef.current) {
|
|
3208
|
+
void loadData(nextLocale, locale);
|
|
3209
|
+
}
|
|
3210
|
+
} else if (mode === "dom" && nextLocale !== defaultLocale) {
|
|
3211
|
+
applyActiveTranslations(document.body);
|
|
3212
|
+
}
|
|
3213
|
+
};
|
|
3214
|
+
}, [defaultLocale, detectLocale, enhancedPathConfig, loadData, locale, mode, trackPageviewOnce]);
|
|
3215
|
+
useHistoryNavigationPatch(onNavigateRef);
|
|
3216
|
+
const setLocale = useCallback7((newLocale) => {
|
|
3217
|
+
void (async () => {
|
|
3218
|
+
if (!allLocales.includes(newLocale)) return;
|
|
3219
|
+
try {
|
|
3220
|
+
localStorage.setItem(LOCALE_STORAGE_KEY, newLocale);
|
|
3221
|
+
} catch (e) {
|
|
3222
|
+
warnDebug("Failed to save locale to localStorage:", e);
|
|
3223
|
+
}
|
|
3224
|
+
isInternalNavigationRef.current = true;
|
|
3225
|
+
isNavigatingRef.current = true;
|
|
3226
|
+
let nextUrl = "";
|
|
3227
|
+
if (routing === "path") {
|
|
3228
|
+
const stripped = stripLocalePrefix(window.location.pathname, allLocales);
|
|
3229
|
+
if (isNonLocalizedPath(stripped, routingConfig.nonLocalizedPaths)) {
|
|
3230
|
+
nextUrl = window.location.href;
|
|
3231
|
+
} else {
|
|
3232
|
+
const pathParts = window.location.pathname.split("/").filter(Boolean);
|
|
3233
|
+
if (allLocales.includes(pathParts[0])) {
|
|
3234
|
+
pathParts.shift();
|
|
3235
|
+
}
|
|
3236
|
+
const basePath = pathParts.join("/");
|
|
3237
|
+
nextUrl = `/${newLocale}${basePath ? "/" + basePath : ""}${window.location.search}${window.location.hash}`;
|
|
3238
|
+
}
|
|
3239
|
+
} else if (routing === "query") {
|
|
3240
|
+
const url = new URL(window.location.href);
|
|
3241
|
+
url.searchParams.set("t", newLocale);
|
|
3242
|
+
nextUrl = url.toString();
|
|
3243
|
+
}
|
|
3244
|
+
if (!nextUrl) nextUrl = window.location.href;
|
|
3245
|
+
window.location.assign(nextUrl);
|
|
3246
|
+
return;
|
|
3247
|
+
})().finally(() => {
|
|
3248
|
+
isInternalNavigationRef.current = false;
|
|
3249
|
+
});
|
|
3250
|
+
}, [allLocales, locale, routing, routingConfig.nonLocalizedPaths]);
|
|
3251
|
+
const loadDataRef = useRef9(loadData);
|
|
3252
|
+
useEffect12(() => {
|
|
3253
|
+
loadDataRef.current = loadData;
|
|
3254
|
+
}, [loadData]);
|
|
3255
|
+
const detectLocaleRef = useRef9(detectLocale);
|
|
3256
|
+
useEffect12(() => {
|
|
3257
|
+
detectLocaleRef.current = detectLocale;
|
|
3258
|
+
}, [detectLocale]);
|
|
3259
|
+
useEffect12(() => {
|
|
3260
|
+
if (editMode && !editSecretKey) {
|
|
3261
|
+
warnDebug("[Lovalingo] Edit Mode is active but no edit_key was provided in the URL.");
|
|
3262
|
+
}
|
|
3263
|
+
}, [editMode, editSecretKey]);
|
|
3264
|
+
useIsomorphicLayoutEffect(() => {
|
|
3265
|
+
const applyLiveMissesQueryParam = () => {
|
|
3266
|
+
if (typeof window === "undefined") return;
|
|
3267
|
+
const url = new URL(window.location.href);
|
|
3268
|
+
const raw = url.searchParams.get(LIVE_MISSES_QUERY_PARAM);
|
|
3269
|
+
if (raw !== "0" && raw !== "1") return;
|
|
3270
|
+
const g = window;
|
|
3271
|
+
if (raw === "1") {
|
|
3272
|
+
if (g.__lovalingoDisableMisses === true) return;
|
|
3273
|
+
g.__lovalingoDisableMisses = false;
|
|
3274
|
+
return;
|
|
3275
|
+
}
|
|
3276
|
+
g.__lovalingoDisableMisses = true;
|
|
3277
|
+
};
|
|
3278
|
+
applyLiveMissesQueryParam();
|
|
3279
|
+
const initialLocale = detectLocaleRef.current();
|
|
3280
|
+
lastNormalizedPathRef.current = processPath(window.location.pathname, enhancedPathConfig);
|
|
3281
|
+
trackPageviewOnce(window.location.pathname + window.location.search);
|
|
3282
|
+
loadDataRef.current(initialLocale);
|
|
3283
|
+
const handleKeyPress = (e) => {
|
|
3284
|
+
if (e.code === editKey && (e.ctrlKey || e.metaKey)) {
|
|
3285
|
+
e.preventDefault();
|
|
3286
|
+
setEditMode((prev) => !prev);
|
|
3287
|
+
}
|
|
3288
|
+
};
|
|
3289
|
+
window.addEventListener("keydown", handleKeyPress);
|
|
3290
|
+
return () => {
|
|
3291
|
+
window.removeEventListener("keydown", handleKeyPress);
|
|
3292
|
+
};
|
|
3293
|
+
}, [editKey, enhancedPathConfig, trackPageviewOnce]);
|
|
3294
|
+
useSitemapLinkTag({ enabled: sitemap, resolvedApiKey, isSeoActive });
|
|
3295
|
+
useLinkAutoPrefix({
|
|
3296
|
+
routing,
|
|
3297
|
+
autoPrefixLinks,
|
|
3298
|
+
allLocales,
|
|
3299
|
+
locale,
|
|
3300
|
+
navigateRef,
|
|
3301
|
+
nonLocalizedPaths: routingConfig.nonLocalizedPaths
|
|
3302
|
+
});
|
|
3303
|
+
useNavigationPrefetch({
|
|
3304
|
+
resolvedApiKey,
|
|
3305
|
+
apiBase,
|
|
3306
|
+
defaultLocale,
|
|
3307
|
+
locale,
|
|
3308
|
+
routing,
|
|
3309
|
+
allLocales,
|
|
3310
|
+
enhancedPathConfig
|
|
3311
|
+
});
|
|
3312
|
+
useStringMissReporting({
|
|
3313
|
+
apiRef,
|
|
3314
|
+
resolvedApiKey,
|
|
3315
|
+
locale,
|
|
3316
|
+
defaultLocale,
|
|
3317
|
+
routing,
|
|
3318
|
+
allLocales,
|
|
3319
|
+
nonLocalizedPaths: routingConfig.nonLocalizedPaths,
|
|
3320
|
+
isLoading,
|
|
3321
|
+
mode
|
|
3322
|
+
});
|
|
3323
|
+
const translateElement = useCallback7((element) => {
|
|
3324
|
+
if (mode !== "dom") return;
|
|
3325
|
+
applyActiveTranslations(element);
|
|
3326
|
+
}, []);
|
|
3327
|
+
const translateDOM = useCallback7(() => {
|
|
3328
|
+
if (mode !== "dom") return;
|
|
3329
|
+
applyActiveTranslations(document.body);
|
|
3330
|
+
}, []);
|
|
3331
|
+
const toggleEditMode = useCallback7(() => {
|
|
3332
|
+
setEditMode((prev) => !prev);
|
|
3333
|
+
}, []);
|
|
3334
|
+
const excludeElement = useCallback7(
|
|
3335
|
+
async (selector) => {
|
|
3336
|
+
if (!editSecretKey) {
|
|
3337
|
+
warnDebug("[Lovalingo] Edit Mode is active but no edit_key was provided in the URL.");
|
|
3338
|
+
return;
|
|
3339
|
+
}
|
|
3340
|
+
const pagePath = lastNormalizedPathRef.current || processPath(window.location.pathname, enhancedPathConfig);
|
|
3341
|
+
await apiRef.current.saveExclusion({
|
|
3342
|
+
selector,
|
|
3343
|
+
type: "css",
|
|
3344
|
+
pagePath,
|
|
3345
|
+
editKey: editSecretKey
|
|
3346
|
+
});
|
|
3347
|
+
const exclusions = await apiRef.current.fetchExclusions();
|
|
3348
|
+
setMarkerEngineExclusions(exclusions);
|
|
3349
|
+
},
|
|
3350
|
+
[editSecretKey, enhancedPathConfig]
|
|
3351
|
+
);
|
|
3352
|
+
useEditModeOverlay({ editMode, excludeElement, setEditMode });
|
|
3353
|
+
const contextValue = {
|
|
3354
|
+
locale,
|
|
3355
|
+
setLocale,
|
|
3356
|
+
isLoading,
|
|
3357
|
+
translationMap: {},
|
|
3358
|
+
config,
|
|
3359
|
+
translateElement,
|
|
3360
|
+
translateDOM,
|
|
3361
|
+
editMode,
|
|
3362
|
+
toggleEditMode,
|
|
3363
|
+
excludeElement
|
|
3364
|
+
};
|
|
3365
|
+
return /* @__PURE__ */ React3.createElement(LovalingoContext.Provider, { value: contextValue }, children, (() => {
|
|
3366
|
+
if (routing !== "path") return true;
|
|
3367
|
+
if (typeof window === "undefined") return true;
|
|
3368
|
+
const stripped = stripLocalePrefix(window.location.pathname, allLocales);
|
|
3369
|
+
return !isNonLocalizedPath(stripped, routingConfig.nonLocalizedPaths);
|
|
3370
|
+
})() && /* @__PURE__ */ React3.createElement(
|
|
3371
|
+
LanguageSwitcher,
|
|
3372
|
+
{
|
|
3373
|
+
locales: allLocales,
|
|
3374
|
+
currentLocale: locale,
|
|
3375
|
+
onLocaleChange: setLocale,
|
|
3376
|
+
position: switcherPosition,
|
|
3377
|
+
offsetY: switcherOffsetY,
|
|
3378
|
+
theme: switcherTheme,
|
|
3379
|
+
branding: {
|
|
3380
|
+
required: Boolean(entitlements?.brandingRequired),
|
|
3381
|
+
enabled: brandingEnabled,
|
|
3382
|
+
href: "https://lovalingo.com"
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
));
|
|
3386
|
+
};
|
|
3387
|
+
|
|
3388
|
+
// src/hooks/useAixster.ts
|
|
3389
|
+
import { useContext as useContext2 } from "react";
|
|
3390
|
+
var useLovalingo = () => {
|
|
3391
|
+
const context = useContext2(LovalingoContext);
|
|
3392
|
+
if (!context) {
|
|
3393
|
+
throw new Error("useLovalingo must be used within LovalingoProvider");
|
|
3394
|
+
}
|
|
3395
|
+
return {
|
|
3396
|
+
locale: context.locale,
|
|
3397
|
+
setLocale: context.setLocale,
|
|
3398
|
+
isLoading: context.isLoading,
|
|
3399
|
+
config: context.config
|
|
3400
|
+
};
|
|
3401
|
+
};
|
|
3402
|
+
|
|
3403
|
+
// src/hooks/useAixsterTranslate.ts
|
|
3404
|
+
import { useContext as useContext3 } from "react";
|
|
3405
|
+
var useLovalingoTranslate = () => {
|
|
3406
|
+
const context = useContext3(LovalingoContext);
|
|
3407
|
+
if (!context) {
|
|
3408
|
+
throw new Error("useLovalingoTranslate must be used within LovalingoProvider");
|
|
3409
|
+
}
|
|
3410
|
+
return {
|
|
3411
|
+
translateElement: context.translateElement,
|
|
3412
|
+
translateDOM: context.translateDOM
|
|
3413
|
+
};
|
|
3414
|
+
};
|
|
3415
|
+
|
|
3416
|
+
// src/hooks/useAixsterEdit.ts
|
|
3417
|
+
import { useContext as useContext4 } from "react";
|
|
3418
|
+
var useLovalingoEdit = () => {
|
|
3419
|
+
const context = useContext4(LovalingoContext);
|
|
3420
|
+
if (!context) {
|
|
3421
|
+
throw new Error("useLovalingoEdit must be used within LovalingoProvider");
|
|
3422
|
+
}
|
|
3423
|
+
return {
|
|
3424
|
+
editMode: context.editMode,
|
|
3425
|
+
toggleEditMode: context.toggleEditMode,
|
|
3426
|
+
excludeElement: context.excludeElement
|
|
3427
|
+
};
|
|
3428
|
+
};
|
|
3429
|
+
|
|
3430
|
+
// src/version.ts
|
|
3431
|
+
var VERSION = "0.6.0";
|
|
3432
|
+
|
|
3433
|
+
export {
|
|
3434
|
+
LanguageSwitcher,
|
|
3435
|
+
LovalingoProvider,
|
|
3436
|
+
useLovalingo,
|
|
3437
|
+
useLovalingoTranslate,
|
|
3438
|
+
useLovalingoEdit,
|
|
3439
|
+
VERSION
|
|
3440
|
+
};
|