@lovalingo/lovalingo 0.5.28 → 0.6.0

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