@lovalingo/lovalingo 0.0.29 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,18 @@
1
- # @lovalingo/lovalingo — Best Weglot & i18n alternative for Vibe Coders, Lovable, v0, Emergent & more
1
+ # @lovalingo/lovalingo
2
2
 
3
- Seamless website translation for React (React Router) and Next.js with **zero‑flash rendering**, **automatic language routing**, and **SEO signals (canonical + hreflang)** built for fast iteration in AI/vibe‑coding workflows.
3
+ Lovalingo is a translation runtime for React (React Router) and Next.js that loads **pre-built artifacts** (JSON bundles + DOM rules) from the Lovalingo backend and applies them with **zero-flash**.
4
4
 
5
- If you ship with Lovable, v0, Emergent, Vite, Claude Code or similar tools, Lovalingo is the fastest way to production‑grade i18n without Weglot overhead.
5
+ It does **not** generate translations in the browser.
6
+
7
+ ## How it works (high level)
8
+
9
+ 1. Your app renders normally (source language).
10
+ 2. Lovalingo loads the current locale’s bundle from the backend.
11
+ 3. The runtime mutates the DOM before paint (and keeps up with route changes + dynamic content).
12
+ 4. Optional: Lovalingo fetches DOM rules (CSS/JS/DOM patches) to fix edge cases (hidden UI, wrapping, etc.).
13
+ 5. Optional SEO: Lovalingo updates `<head>` (canonical + hreflang + basic meta) using `seo-bundle`.
14
+
15
+ All artifacts are produced server-side by the pipeline (render → audit → deterministic translate → optional fix loop).
6
16
 
7
17
  ## Installation
8
18
 
@@ -10,108 +20,75 @@ If you ship with Lovable, v0, Emergent, Vite, Claude Code or similar tools, Lova
10
20
  npm install @lovalingo/lovalingo react-router-dom
11
21
  ```
12
22
 
13
- ## Quick Start
23
+ ## React Router
14
24
 
15
- Lovalingo **erzeugt Übersetzungen nicht im Browser**. Die Runtime lädt fertige Artefakte (JSON‑Bundle + DOM‑Rules) vom Backend; die server‑seitige Pipeline rendert Seiten, extrahiert Marker/Strings, übersetzt deterministisch und erzeugt bei Bedarf DOM‑Fixes.
16
-
17
- ### Option 1: Path Mode (Recommended - Automatic Language URLs)
18
-
19
- **One-line setup** that automatically handles language routing like `/en/pricing`, `/fr/pricing`, `/de/pricing`:
25
+ ### Query mode (default)
20
26
 
21
27
  ```tsx
22
- import { LangRouter, LovalingoProvider } from '@lovalingo/lovalingo';
23
- import { Routes, Route } from 'react-router-dom';
24
- import { useRef } from 'react';
25
-
26
- function App() {
27
- const navigateRef = useRef();
28
+ import { BrowserRouter } from "react-router-dom";
29
+ import { LovalingoProvider } from "@lovalingo/lovalingo";
28
30
 
31
+ export function App() {
29
32
  return (
30
- <LangRouter defaultLang="en" langs={['en', 'fr', 'de', 'es']} navigateRef={navigateRef}>
33
+ <BrowserRouter>
31
34
  <LovalingoProvider
32
35
  publicAnonKey="aix_your_public_anon_key"
33
36
  defaultLocale="en"
34
- locales={['en', 'fr', 'de', 'es']}
35
- routing="path"
36
- navigateRef={navigateRef}
37
+ locales={["en", "de", "fr"]}
38
+ routing="query"
37
39
  >
38
- <Routes>
39
- {/* No language prefix needed! */}
40
- <Route path="/" element={<Home />} />
41
- <Route path="home" element={<Home />} />
42
- <Route path="pricing" element={<Pricing />} />
43
- <Route path="about" element={<About />} />
44
- </Routes>
40
+ <YourApp />
45
41
  </LovalingoProvider>
46
- </LangRouter>
42
+ </BrowserRouter>
47
43
  );
48
44
  }
49
45
  ```
50
46
 
51
- **Important:** Pass the same `navigateRef` to both `<LangRouter>` and `<LovalingoProvider>` for proper URL synchronization.
47
+ URLs look like: `/pricing?t=de`.
52
48
 
53
- **Common error (React Router v6):** If you see `LovalingoProvider is not a <Route> component`, you’re either:
54
- - on an older `@lovalingo/lovalingo` version (upgrade), or
55
- - you accidentally placed `<LovalingoProvider>` directly inside `<Routes>`.
49
+ ### Path mode (SEO-friendly URLs)
56
50
 
57
- Correct:
58
51
  ```tsx
59
- <LovalingoProvider ...>
60
- <Routes>...</Routes>
61
- </LovalingoProvider>
62
- ```
63
-
64
- Incorrect:
65
- ```tsx
66
- <Routes>
67
- <LovalingoProvider ... />
68
- </Routes>
69
- ```
70
-
71
- URLs automatically work as:
72
- - `/en/`, `/en/home`, `/en/pricing`, `/en/about`
73
- - `/fr/`, `/fr/home`, `/fr/pricing`, `/fr/about`
74
- - `/de/`, `/de/home`, `/de/pricing`, `/de/about`
52
+ import { useRef } from "react";
53
+ import { Routes, Route } from "react-router-dom";
54
+ import { LangRouter, LovalingoProvider } from "@lovalingo/lovalingo";
75
55
 
76
- ### Option 2: Query Mode (Simple - Query Parameters)
56
+ export function App() {
57
+ const navigateRef = useRef<((path: string) => void) | undefined>(undefined);
77
58
 
78
- Use query parameters like `/pricing?t=fr`:
79
-
80
- ```tsx
81
- import { LovalingoProvider } from '@lovalingo/lovalingo';
82
- import { BrowserRouter } from 'react-router-dom';
83
-
84
- function App() {
85
59
  return (
86
- <BrowserRouter>
60
+ <LangRouter defaultLang="en" langs={["en", "de", "fr"]} navigateRef={navigateRef}>
87
61
  <LovalingoProvider
88
62
  publicAnonKey="aix_your_public_anon_key"
89
63
  defaultLocale="en"
90
- locales={['en', 'de', 'fr', 'es']}
91
- routing="query"
64
+ locales={["en", "de", "fr"]}
65
+ routing="path"
66
+ navigateRef={navigateRef}
92
67
  >
93
- <YourApp />
68
+ <Routes>
69
+ <Route path="/" element={<Home />} />
70
+ <Route path="pricing" element={<Pricing />} />
71
+ <Route path="about" element={<About />} />
72
+ </Routes>
94
73
  </LovalingoProvider>
95
- </BrowserRouter>
74
+ </LangRouter>
96
75
  );
97
76
  }
98
77
  ```
99
78
 
100
- ### Next.js
79
+ URLs look like: `/de/pricing`.
80
+
81
+ ## Next.js (App Router)
101
82
 
102
83
  ```tsx
103
84
  // app/layout.tsx
104
- import { LovalingoProvider } from '@lovalingo/lovalingo';
85
+ import { LovalingoProvider } from "@lovalingo/lovalingo";
105
86
 
106
- export default function RootLayout({ children }) {
87
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
107
88
  return (
108
89
  <html>
109
90
  <body>
110
- <LovalingoProvider
111
- publicAnonKey="aix_your_public_anon_key"
112
- defaultLocale="en"
113
- locales={['en', 'de', 'fr', 'es']}
114
- >
91
+ <LovalingoProvider publicAnonKey="aix_your_public_anon_key" defaultLocale="en" locales={["en", "de", "fr"]}>
115
92
  {children}
116
93
  </LovalingoProvider>
117
94
  </body>
@@ -120,124 +97,27 @@ export default function RootLayout({ children }) {
120
97
  }
121
98
  ```
122
99
 
123
- ## Configuration
124
-
125
- ```tsx
126
- <LovalingoProvider
127
- publicAnonKey="aix_xxx" // Required: Your Lovalingo Public Anon Key (safe to expose)
128
- defaultLocale="en" // Required: Source language
129
- locales={['en', 'de', 'fr']} // Required: Supported languages
130
- apiBase="https://..." // Optional: Custom API endpoint
131
- routing="query" // Optional: 'query' | 'path' (default: 'query')
132
- autoPrefixLinks={true} // Optional (path mode): keep locale when the app renders absolute links like "/pricing"
133
- switcherPosition="bottom-right" // Optional: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
134
- switcherOffsetY={20} // Optional: Vertical offset in pixels
135
- editMode={false} // Optional: Enable edit mode
136
- editKey="KeyE" // Optional: Keyboard shortcut for edit mode
137
- mode="dom" // Optional: 'dom' | 'context' (default: 'dom')
138
- pathNormalization={{ enabled: true }}// Optional: normalize paths for stable translation keys (default: enabled)
139
- seo={true} // Optional: auto canonical + hreflang + sitemap link (default: true)
140
- sitemap={true} // Optional: auto-inject sitemap link (default: true)
141
- >
142
- {children}
143
- </LovalingoProvider>
144
- ```
145
-
146
- ### Optional: Inject via `index.html`
100
+ ## SEO (canonical + hreflang + meta)
147
101
 
148
- If your tooling prefers you to avoid hardcoding the key inside JSX, you can inject it at runtime:
149
-
150
- ```html
151
- <meta name="lovalingo-public-anon-key" content="aix_xxx" />
152
- <script>
153
- window.__LOVALINGO_PUBLIC_ANON_KEY__ = "aix_xxx";
154
- </script>
155
- ```
156
-
157
- Then you may omit `publicAnonKey` and Lovalingo will read it from the meta tag or `window.__LOVALINGO_PUBLIC_ANON_KEY__`.
158
-
159
- ### Routing Modes
160
-
161
- - **`query`** (default): Language in query parameter `/pricing?t=fr`
162
- - Use with standard React Router setup
163
- - No URL structure changes needed
164
-
165
- - **`path`**: Language in URL path `/fr/pricing`
166
- - Use with `<LangRouter>` wrapper
167
- - Automatic language routing
168
- - SEO-optimized URLs
169
- - Non-localized by default: `/auth`, `/login`, `/signup` (no `/:lang/` prefix)
170
- - Examples:
171
- - `/en/` → English home
172
- - `/de/pricing` → German pricing
173
- - `/fr/about` → French about
174
-
175
- ## Path Mode Helpers (v0.0.x+)
176
-
177
- ### LangLink - Language-Aware Links
102
+ Enabled by default. Disable if you already manage `<head>` yourself:
178
103
 
179
104
  ```tsx
180
- import { LangLink } from '@lovalingo/lovalingo';
181
-
182
- function Navigation() {
183
- return (
184
- <nav>
185
- {/* Automatically becomes /en/pricing or /fr/pricing */}
186
- <LangLink to="pricing">Pricing</LangLink>
187
- <LangLink to="about">About</LangLink>
188
- </nav>
189
- );
190
- }
105
+ <LovalingoProvider seo={false} ... />
191
106
  ```
192
107
 
193
- ### useLang - Get Current Language
108
+ ## Sitemap link tag
194
109
 
195
- ```tsx
196
- import { useLang } from '@lovalingo/lovalingo';
197
-
198
- function MyComponent() {
199
- const lang = useLang(); // 'en', 'fr', 'de', etc.
200
- return <div>Current language: {lang}</div>;
201
- }
202
- ```
203
-
204
- ### useLangNavigate - Programmatic Navigation
110
+ By default Lovalingo injects `<link rel="sitemap" href="/sitemap.xml">` for discovery. Disable it:
205
111
 
206
112
  ```tsx
207
- import { useLangNavigate } from '@lovalingo/lovalingo';
208
-
209
- function MyComponent() {
210
- const navigate = useLangNavigate();
211
-
212
- const goToPricing = () => {
213
- navigate('pricing'); // Goes to /en/pricing or /fr/pricing automatically
214
- };
215
-
216
- return <button onClick={goToPricing}>View Pricing</button>;
217
- }
113
+ <LovalingoProvider sitemap={false} ... />
218
114
  ```
219
115
 
220
- ## Core Hooks
116
+ You still need to serve `/sitemap.xml` on your own domain (recommended: reverse-proxy to Lovalingo’s `generate-sitemap` endpoint).
221
117
 
222
- ### useLovalingo
223
-
224
- Access locale state and switching:
225
-
226
- ```tsx
227
- import { useLovalingo } from '@lovalingo/lovalingo';
228
-
229
- function MyComponent() {
230
- const { locale, setLocale, isLoading, config } = useLovalingo();
231
-
232
- return (
233
- <button onClick={() => setLocale('de')}>
234
- Switch to German
235
- </button>
236
- );
237
- }
238
- ```
118
+ ## License
239
119
 
240
- ### useLovalingoTranslate
120
+ Commercial license (not open source). See `react-package/LICENSE`.
241
121
 
242
122
  Manual translation control:
243
123
 
@@ -295,6 +175,23 @@ function MyComponent() {
295
175
  - ✅ RTL ready: automatically sets `<html dir="rtl">` for `ar/he/fa/ur`
296
176
  - ✅ Optional Context Mode: `<AutoTranslate>` with hash-based caching for React text nodes
297
177
 
178
+ ## Language Switcher
179
+
180
+ Lovalingo includes a floating language switcher.
181
+
182
+ ```tsx
183
+ <LovalingoProvider
184
+ publicAnonKey="aix_xxx"
185
+ defaultLocale="en"
186
+ locales={["en", "de", "fr"]}
187
+ switcherPosition="bottom-right"
188
+ switcherOffsetY={20}
189
+ switcherTheme="light" // "dark" | "light" (default: "dark")
190
+ >
191
+ <App />
192
+ </LovalingoProvider>
193
+ ```
194
+
298
195
  ## SEO (Canonical + hreflang)
299
196
 
300
197
  Lovalingo can keep `<head>` SEO signals in sync with the active locale:
@@ -3,11 +3,13 @@ import { LovalingoContext } from '../context/LovalingoContext';
3
3
  import { LovalingoAPI } from '../utils/api';
4
4
  import { Translator } from '../utils/translator';
5
5
  import { applyDomRules } from '../utils/domRules';
6
+ import { startMarkerEngine } from '../utils/markerEngine';
7
+ import { logDebug, warnDebug, errorDebug } from '../utils/logger';
6
8
  import { LanguageSwitcher } from './LanguageSwitcher';
7
9
  import { NavigationOverlay } from './NavigationOverlay';
8
10
  const LOCALE_STORAGE_KEY = 'Lovalingo_locale';
9
11
  export const LovalingoProvider = ({ children, apiKey: apiKeyProp, publicAnonKey, defaultLocale, locales, apiBase = 'https://leuskvkajliuzalrlwhw.supabase.co', routing = 'query', // Default to query mode (backward compatible)
10
- autoPrefixLinks = true, autoApplyRules = true, switcherPosition = 'bottom-right', switcherOffsetY = 20, editMode: initialEditMode = false, editKey = 'KeyE', pathNormalization = { enabled: true }, // Enable by default
12
+ autoPrefixLinks = true, autoApplyRules = true, switcherPosition = 'bottom-right', switcherOffsetY = 20, switcherTheme = 'dark', editMode: initialEditMode = false, editKey = 'KeyE', pathNormalization = { enabled: true }, // Enable by default
11
13
  mode = 'dom', // Default to legacy DOM mode for backward compatibility
12
14
  sitemap = true, // Default: true - Auto-inject sitemap link tag
13
15
  seo = true, // Default: true - Can be disabled per project entitlements
@@ -73,6 +75,7 @@ navigateRef, // For path mode routing
73
75
  autoPrefixLinks,
74
76
  switcherPosition,
75
77
  switcherOffsetY,
78
+ switcherTheme,
76
79
  editMode: initialEditMode,
77
80
  editKey,
78
81
  pathNormalization,
@@ -204,9 +207,14 @@ navigateRef, // For path mode routing
204
207
  }
205
208
  }
206
209
  catch (e) {
207
- console.warn("[Lovalingo] updateSeoLinks() failed:", e);
210
+ warnDebug("[Lovalingo] updateSeoLinks() failed:", e);
208
211
  }
209
212
  }, [isSeoActive]);
213
+ // Marker engine: always mark full DOM content for deterministic pipeline extraction.
214
+ useEffect(() => {
215
+ const stop = startMarkerEngine({ throttleMs: 120 });
216
+ return () => stop();
217
+ }, []);
210
218
  // Detect locale from URL or localStorage
211
219
  const detectLocale = useCallback(() => {
212
220
  // 1. Check URL first based on routing mode
@@ -234,7 +242,7 @@ navigateRef, // For path mode routing
234
242
  }
235
243
  catch (e) {
236
244
  // localStorage might be unavailable (SSR, private browsing)
237
- console.warn('localStorage not available:', e);
245
+ warnDebug('localStorage not available:', e);
238
246
  }
239
247
  // 3. Default locale
240
248
  return defaultLocale;
@@ -282,7 +290,7 @@ navigateRef, // For path mode routing
282
290
  const cachedDomRules = domRulesCacheRef.current.get(cacheKey);
283
291
  if (cachedEntry && cachedExclusions) {
284
292
  // CACHE HIT - Use cached data immediately (FAST!)
285
- console.log(`[Lovalingo] Using cached translations for ${targetLocale} on ${currentPath}`);
293
+ logDebug(`[Lovalingo] Using cached translations for ${targetLocale} on ${currentPath}`);
286
294
  translatorRef.current.setTranslations(cachedEntry.translations);
287
295
  translatorRef.current.setExclusions(cachedExclusions);
288
296
  if (mode === 'dom') {
@@ -304,7 +312,7 @@ navigateRef, // For path mode routing
304
312
  if (isNavigatingRef.current) {
305
313
  return;
306
314
  }
307
- console.log(`[Lovalingo] 🔄 Retry scan for late-rendering content`);
315
+ logDebug(`[Lovalingo] 🔄 Retry scan for late-rendering content`);
308
316
  if (mode === 'dom') {
309
317
  translatorRef.current.translateDOM();
310
318
  }
@@ -320,11 +328,11 @@ navigateRef, // For path mode routing
320
328
  return;
321
329
  }
322
330
  // CACHE MISS - Fetch from API
323
- console.log(`[Lovalingo] Fetching translations for ${targetLocale} on ${currentPath}`);
331
+ logDebug(`[Lovalingo] Fetching translations for ${targetLocale} on ${currentPath}`);
324
332
  setIsLoading(true);
325
333
  try {
326
334
  if (previousLocale && previousLocale !== defaultLocale) {
327
- console.log(`[Lovalingo] Switching from ${previousLocale} to ${targetLocale}`);
335
+ logDebug(`[Lovalingo] Switching from ${previousLocale} to ${targetLocale}`);
328
336
  }
329
337
  const [bundle, exclusions, domRules] = await Promise.all([
330
338
  apiRef.current.fetchBundle(targetLocale),
@@ -362,7 +370,7 @@ navigateRef, // For path mode routing
362
370
  if (isNavigatingRef.current) {
363
371
  return;
364
372
  }
365
- console.log(`[Lovalingo] 🔄 Retry scan for late-rendering content`);
373
+ logDebug(`[Lovalingo] 🔄 Retry scan for late-rendering content`);
366
374
  if (mode === "dom") {
367
375
  translatorRef.current.translateDOM();
368
376
  }
@@ -376,7 +384,7 @@ navigateRef, // For path mode routing
376
384
  }
377
385
  }
378
386
  catch (error) {
379
- console.error('Error loading translations:', error);
387
+ errorDebug('Error loading translations:', error);
380
388
  if (showOverlay)
381
389
  setIsNavigationLoading(false);
382
390
  }
@@ -396,7 +404,7 @@ navigateRef, // For path mode routing
396
404
  localStorage.setItem(LOCALE_STORAGE_KEY, newLocale);
397
405
  }
398
406
  catch (e) {
399
- console.warn('Failed to save locale to localStorage:', e);
407
+ warnDebug('Failed to save locale to localStorage:', e);
400
408
  }
401
409
  isInternalNavigationRef.current = true;
402
410
  // Show navigation overlay immediately (only when a non-default locale is involved)
@@ -788,7 +796,7 @@ navigateRef, // For path mode routing
788
796
  };
789
797
  return (React.createElement(LovalingoContext.Provider, { value: contextValue },
790
798
  children,
791
- React.createElement(LanguageSwitcher, { locales: allLocales, currentLocale: locale, onLocaleChange: setLocale, position: switcherPosition, offsetY: switcherOffsetY, branding: entitlements?.brandingRequired
799
+ React.createElement(LanguageSwitcher, { locales: allLocales, currentLocale: locale, onLocaleChange: setLocale, position: switcherPosition, offsetY: switcherOffsetY, theme: switcherTheme, branding: entitlements?.brandingRequired
792
800
  ? { required: true, href: "https://lovalingo.com" }
793
801
  : undefined }),
794
802
  React.createElement(NavigationOverlay, { isVisible: isNavigationLoading })));
@@ -5,6 +5,7 @@ interface LanguageSwitcherProps {
5
5
  onLocaleChange: (locale: string) => void;
6
6
  position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
7
7
  offsetY?: number;
8
+ theme?: 'dark' | 'light';
8
9
  branding?: {
9
10
  required?: boolean;
10
11
  label?: string;
@@ -22,12 +22,39 @@ const LANGUAGE_FLAGS = {
22
22
  no: '🇳🇴',
23
23
  fi: '🇫🇮',
24
24
  };
25
- export const LanguageSwitcher = ({ locales, currentLocale, onLocaleChange, position = 'bottom-right', offsetY = 20, branding, }) => {
25
+ export const LanguageSwitcher = ({ locales, currentLocale, onLocaleChange, position = 'bottom-right', offsetY = 20, theme = 'dark', branding, }) => {
26
26
  const [isOpen, setIsOpen] = useState(false);
27
27
  const containerRef = useRef(null);
28
28
  const isRight = position.endsWith('right');
29
29
  const isTop = position.startsWith('top');
30
30
  const isBottom = position.startsWith('bottom');
31
+ const tokens = theme === 'light'
32
+ ? {
33
+ surfaceBg: 'rgba(255, 255, 255, 0.93)',
34
+ surfaceHoverBg: 'rgba(245, 245, 245, 0.95)',
35
+ text: 'rgba(13, 13, 13, 0.96)',
36
+ textMuted: 'rgba(13, 13, 13, 0.74)',
37
+ divider: 'rgba(0, 0, 0, 0.10)',
38
+ insetHighlight: 'inset 0 0 1px rgba(0, 0, 0, 0.10)',
39
+ tabShadowRight: '-2px 0 8px rgba(0, 0, 0, 0.12), inset 0 0 1px rgba(0, 0, 0, 0.10)',
40
+ tabShadowLeft: '2px 0 8px rgba(0, 0, 0, 0.12), inset 0 0 1px rgba(0, 0, 0, 0.10)',
41
+ tabHoverShadowRight: '-4px 0 16px rgba(0, 0, 0, 0.16), inset 0 0 1px rgba(0, 0, 0, 0.12)',
42
+ tabHoverShadowLeft: '4px 0 16px rgba(0, 0, 0, 0.16), inset 0 0 1px rgba(0, 0, 0, 0.12)',
43
+ panelShadow: '0 8px 24px rgba(0, 0, 0, 0.14), inset 0 0 1px rgba(0, 0, 0, 0.10)',
44
+ }
45
+ : {
46
+ surfaceBg: 'rgba(26, 26, 26, 0.93)',
47
+ surfaceHoverBg: 'rgba(35, 35, 35, 0.95)',
48
+ text: 'rgba(255, 255, 255, 0.98)',
49
+ textMuted: 'rgba(255, 255, 255, 0.82)',
50
+ divider: 'rgba(255, 255, 255, 0.12)',
51
+ insetHighlight: 'inset 0 0 1px rgba(255, 255, 255, 0.10)',
52
+ tabShadowRight: '-2px 0 8px rgba(0, 0, 0, 0.2), inset 0 0 1px rgba(255, 255, 255, 0.10)',
53
+ tabShadowLeft: '2px 0 8px rgba(0, 0, 0, 0.2), inset 0 0 1px rgba(255, 255, 255, 0.10)',
54
+ tabHoverShadowRight: '-4px 0 16px rgba(0, 0, 0, 0.3), inset 0 0 1px rgba(255, 255, 255, 0.15)',
55
+ tabHoverShadowLeft: '4px 0 16px rgba(0, 0, 0, 0.3), inset 0 0 1px rgba(255, 255, 255, 0.15)',
56
+ panelShadow: '0 8px 24px rgba(0, 0, 0, 0.3), inset 0 0 1px rgba(255, 255, 255, 0.10)',
57
+ };
31
58
  // Order locales: active first, then others
32
59
  const orderedLocales = [
33
60
  currentLocale,
@@ -80,17 +107,15 @@ export const LanguageSwitcher = ({ locales, currentLocale, onLocaleChange, posit
80
107
  width: '44px',
81
108
  height: '50px',
82
109
  borderRadius: isRight ? '12px 0 0 12px' : '0 12px 12px 0',
83
- background: 'rgba(26, 26, 26, 0.93)',
110
+ background: tokens.surfaceBg,
84
111
  backdropFilter: 'blur(12px)',
85
- boxShadow: isRight
86
- ? '-2px 0 8px rgba(0, 0, 0, 0.2), inset 0 0 1px rgba(255, 255, 255, 0.1)'
87
- : '2px 0 8px rgba(0, 0, 0, 0.2), inset 0 0 1px rgba(255, 255, 255, 0.1)',
112
+ boxShadow: isRight ? tokens.tabShadowRight : tokens.tabShadowLeft,
88
113
  cursor: 'pointer',
89
114
  fontSize: '20px',
90
115
  transition: 'all 0.2s ease',
91
116
  userSelect: 'none',
92
117
  border: 'none',
93
- color: 'white',
118
+ color: tokens.text,
94
119
  };
95
120
  const panelStyles = {
96
121
  position: 'absolute',
@@ -102,14 +127,14 @@ export const LanguageSwitcher = ({ locales, currentLocale, onLocaleChange, posit
102
127
  : `translateY(-50%) translateX(${isRight ? '12px' : '-12px'})`,
103
128
  opacity: isOpen ? 1 : 0,
104
129
  pointerEvents: isOpen ? 'auto' : 'none',
105
- background: 'rgba(26, 26, 26, 0.93)',
130
+ background: tokens.surfaceBg,
106
131
  backdropFilter: 'blur(12px)',
107
132
  borderRadius: '16px',
108
133
  padding: '10px 12px',
109
134
  display: 'flex',
110
135
  flexDirection: 'column',
111
136
  gap: '10px',
112
- boxShadow: '0 8px 24px rgba(0, 0, 0, 0.3), inset 0 0 1px rgba(255, 255, 255, 0.1)',
137
+ boxShadow: tokens.panelShadow,
113
138
  transition: 'opacity 0.25s ease, transform 0.25s ease',
114
139
  };
115
140
  const localeRowStyles = {
@@ -122,14 +147,14 @@ export const LanguageSwitcher = ({ locales, currentLocale, onLocaleChange, posit
122
147
  alignItems: 'center',
123
148
  gap: '8px',
124
149
  paddingTop: '8px',
125
- borderTop: '1px solid rgba(255, 255, 255, 0.12)',
150
+ borderTop: `1px solid ${tokens.divider}`,
126
151
  fontSize: '12px',
127
- color: 'rgba(255, 255, 255, 0.82)',
152
+ color: tokens.textMuted,
128
153
  userSelect: 'none',
129
154
  whiteSpace: 'nowrap',
130
155
  };
131
156
  const badgeLinkStyles = {
132
- color: 'rgba(255, 255, 255, 0.92)',
157
+ color: tokens.text,
133
158
  textDecoration: 'none',
134
159
  display: 'inline-flex',
135
160
  alignItems: 'center',
@@ -155,15 +180,11 @@ export const LanguageSwitcher = ({ locales, currentLocale, onLocaleChange, posit
155
180
  return (React.createElement("div", { ref: containerRef, style: containerStyles, "data-Lovalingo-exclude": "true" },
156
181
  React.createElement("div", { style: rootStyles },
157
182
  React.createElement("button", { style: tabStyles, onClick: () => setIsOpen(!isOpen), onMouseEnter: (e) => {
158
- e.currentTarget.style.background = 'rgba(35, 35, 35, 0.95)';
159
- e.currentTarget.style.boxShadow = isRight
160
- ? '-4px 0 16px rgba(0, 0, 0, 0.3), inset 0 0 1px rgba(255, 255, 255, 0.15)'
161
- : '4px 0 16px rgba(0, 0, 0, 0.3), inset 0 0 1px rgba(255, 255, 255, 0.15)';
183
+ e.currentTarget.style.background = tokens.surfaceHoverBg;
184
+ e.currentTarget.style.boxShadow = isRight ? tokens.tabHoverShadowRight : tokens.tabHoverShadowLeft;
162
185
  }, onMouseLeave: (e) => {
163
- e.currentTarget.style.background = 'rgba(26, 26, 26, 0.93)';
164
- e.currentTarget.style.boxShadow = isRight
165
- ? '-2px 0 8px rgba(0, 0, 0, 0.2), inset 0 0 1px rgba(255, 255, 255, 0.1)'
166
- : '2px 0 8px rgba(0, 0, 0, 0.2), inset 0 0 1px rgba(255, 255, 255, 0.1)';
186
+ e.currentTarget.style.background = tokens.surfaceBg;
187
+ e.currentTarget.style.boxShadow = isRight ? tokens.tabShadowRight : tokens.tabShadowLeft;
167
188
  }, "aria-label": "Open language switcher", "aria-expanded": isOpen }, LANGUAGE_FLAGS[currentLocale] || '🌐'),
168
189
  React.createElement("div", { style: panelStyles, role: "toolbar", "aria-label": "Language options" },
169
190
  React.createElement("div", { style: localeRowStyles }, orderedLocales.map((locale) => (React.createElement("button", { key: locale, style: flagButtonStyles(locale), onClick: (e) => {
@@ -202,5 +223,5 @@ export const LanguageSwitcher = ({ locales, currentLocale, onLocaleChange, posit
202
223
  React.createElement("span", null,
203
224
  branding.label || 'Localized by',
204
225
  " ",
205
- React.createElement("strong", { style: { color: 'white' } }, "Lovalingo")))))))));
226
+ React.createElement("strong", { "data-no-translate": true, style: { color: tokens.text } }, "Lovalingo")))))))));
206
227
  };
package/dist/types.d.ts CHANGED
@@ -4,9 +4,6 @@ export interface LovalingoConfig {
4
4
  * Public project key (safe to expose in the browser).
5
5
  */
6
6
  publicAnonKey?: string;
7
- /**
8
- * @deprecated Use `publicAnonKey`.
9
- */
10
7
  apiKey?: string;
11
8
  defaultLocale: string;
12
9
  locales: string[];
@@ -15,6 +12,7 @@ export interface LovalingoConfig {
15
12
  autoPrefixLinks?: boolean;
16
13
  switcherPosition?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
17
14
  switcherOffsetY?: number;
15
+ switcherTheme?: 'dark' | 'light';
18
16
  editMode?: boolean;
19
17
  editKey?: string;
20
18
  pathNormalization?: PathNormalizationConfig;
package/dist/utils/api.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { processPath } from './pathNormalizer';
2
+ import { warnDebug, errorDebug } from './logger';
2
3
  export class LovalingoAPI {
3
4
  constructor(apiKey, apiBase, pathConfig) {
4
5
  this.entitlements = null;
@@ -11,10 +12,10 @@ export class LovalingoAPI {
11
12
  }
12
13
  warnMissingApiKey(action) {
13
14
  // Avoid hard-crashing apps; make the failure mode obvious.
14
- console.warn(`[Lovalingo] Missing public project key: ${action} was skipped. Pass publicAnonKey (or apiKey) to <LovalingoProvider ...> (or set VITE_LOVALINGO_PUBLIC_ANON_KEY / VITE_LOVALINGO_API_KEY).`);
15
+ warnDebug(`[Lovalingo] Missing public project key: ${action} was skipped. Pass publicAnonKey to <LovalingoProvider ...> (or set VITE_LOVALINGO_PUBLIC_ANON_KEY).`);
15
16
  }
16
17
  logActivationRequired(context, response) {
17
- console.error(`[Lovalingo] ${context} blocked (HTTP ${response.status}). ` +
18
+ errorDebug(`[Lovalingo] ${context} blocked (HTTP ${response.status}). ` +
18
19
  `This project is not activated yet. ` +
19
20
  `Publish a public manifest at "/.well-known/lovalingo.json" on your domain, ` +
20
21
  `then verify it in the Lovalingo dashboard to activate translations + SEO.`);
@@ -129,7 +130,7 @@ export class LovalingoAPI {
129
130
  }));
130
131
  }
131
132
  catch (error) {
132
- console.error('Error fetching translations:', error);
133
+ errorDebug('Error fetching translations:', error);
133
134
  return [];
134
135
  }
135
136
  }
@@ -184,7 +185,7 @@ export class LovalingoAPI {
184
185
  return Array.isArray(data.exclusions) ? data.exclusions : [];
185
186
  }
186
187
  catch (error) {
187
- console.error('Error fetching exclusions:', error);
188
+ errorDebug('Error fetching exclusions:', error);
188
189
  return [];
189
190
  }
190
191
  }
@@ -210,7 +211,7 @@ export class LovalingoAPI {
210
211
  return Array.isArray(data?.rules) ? data.rules : [];
211
212
  }
212
213
  catch (error) {
213
- console.error('Error fetching DOM rules:', error);
214
+ errorDebug('Error fetching DOM rules:', error);
214
215
  return [];
215
216
  }
216
217
  }
@@ -230,7 +231,7 @@ export class LovalingoAPI {
230
231
  }
231
232
  }
232
233
  catch (error) {
233
- console.error('Error saving exclusion:', error);
234
+ errorDebug('Error saving exclusion:', error);
234
235
  throw error;
235
236
  }
236
237
  }
@@ -0,0 +1,3 @@
1
+ export declare function logDebug(...args: unknown[]): void;
2
+ export declare function warnDebug(...args: unknown[]): void;
3
+ export declare function errorDebug(...args: unknown[]): void;
@@ -0,0 +1,23 @@
1
+ function isDebugEnabled() {
2
+ if (typeof globalThis === "undefined")
3
+ return false;
4
+ const value = globalThis.__lovalingoDebug;
5
+ if (value === true || value === "true" || value === 1)
6
+ return true;
7
+ return false;
8
+ }
9
+ export function logDebug(...args) {
10
+ if (!isDebugEnabled())
11
+ return;
12
+ console.log(...args);
13
+ }
14
+ export function warnDebug(...args) {
15
+ if (!isDebugEnabled())
16
+ return;
17
+ console.warn(...args);
18
+ }
19
+ export function errorDebug(...args) {
20
+ if (!isDebugEnabled())
21
+ return;
22
+ console.error(...args);
23
+ }
@@ -0,0 +1,21 @@
1
+ type MarkerStats = {
2
+ totalTextNodes: number;
3
+ markedNodes: number;
4
+ skippedUnsafeNodes: number;
5
+ skippedExcludedNodes: number;
6
+ skippedNonTranslatableNodes: number;
7
+ totalChars: number;
8
+ markedChars: number;
9
+ skippedUnsafeChars: number;
10
+ skippedExcludedChars: number;
11
+ skippedNonTranslatableChars: number;
12
+ coverageRatio: number;
13
+ coverageRatioChars: number;
14
+ };
15
+ type MarkerEngineOptions = {
16
+ throttleMs?: number;
17
+ };
18
+ export declare function startMarkerEngine(options?: MarkerEngineOptions): typeof stopMarkerEngine;
19
+ export declare function stopMarkerEngine(): void;
20
+ export declare function getMarkerStats(): MarkerStats;
21
+ export {};
@@ -0,0 +1,279 @@
1
+ import { hashContent } from "./hash";
2
+ const DEFAULT_THROTTLE_MS = 150;
3
+ const EXCLUDE_SELECTOR = "[data-lovalingo-exclude],[data-notranslate],[translate-no],[data-no-translate]";
4
+ const MARKER_SELECTOR = "[data-lovalingo-original]";
5
+ const UNSAFE_CONTAINER_TAGS = new Set(["script", "style", "noscript", "template", "svg", "canvas"]);
6
+ const DIRECT_MARK_TAGS = new Set(["option", "textarea"]);
7
+ const ATTRIBUTE_MARKS = [
8
+ { attr: "title", marker: "data-lovalingo-title-original" },
9
+ { attr: "aria-label", marker: "data-lovalingo-aria-label-original" },
10
+ { attr: "placeholder", marker: "data-lovalingo-placeholder-original" },
11
+ ];
12
+ const unsafeSelector = Array.from(UNSAFE_CONTAINER_TAGS).join(",");
13
+ let observer = null;
14
+ let scheduled = null;
15
+ let running = false;
16
+ let lastStats = buildEmptyStats();
17
+ let throttleMs = DEFAULT_THROTTLE_MS;
18
+ function buildEmptyStats() {
19
+ return {
20
+ totalTextNodes: 0,
21
+ markedNodes: 0,
22
+ skippedUnsafeNodes: 0,
23
+ skippedExcludedNodes: 0,
24
+ skippedNonTranslatableNodes: 0,
25
+ totalChars: 0,
26
+ markedChars: 0,
27
+ skippedUnsafeChars: 0,
28
+ skippedExcludedChars: 0,
29
+ skippedNonTranslatableChars: 0,
30
+ coverageRatio: 0,
31
+ coverageRatioChars: 0,
32
+ };
33
+ }
34
+ function setGlobalStats(stats) {
35
+ lastStats = stats;
36
+ if (typeof window === "undefined")
37
+ return;
38
+ window.__lovalingoMarkersReady = true;
39
+ window.__lovalingoMarkerStats = stats;
40
+ }
41
+ function isExcludedElement(el) {
42
+ if (!el)
43
+ return false;
44
+ return Boolean(el.closest(EXCLUDE_SELECTOR));
45
+ }
46
+ function findUnsafeContainer(el) {
47
+ if (!el)
48
+ return null;
49
+ if (!unsafeSelector)
50
+ return null;
51
+ return el.closest(unsafeSelector);
52
+ }
53
+ function getStableKey(el) {
54
+ const owner = el.closest("[data-lovalingo-key]");
55
+ const key = owner?.getAttribute("data-lovalingo-key") || "";
56
+ return key.trim();
57
+ }
58
+ function getElementIndex(el) {
59
+ const parent = el.parentElement;
60
+ if (!parent)
61
+ return 0;
62
+ const children = Array.from(parent.children);
63
+ const idx = children.indexOf(el);
64
+ return idx >= 0 ? idx : 0;
65
+ }
66
+ function getTextNodeIndex(node) {
67
+ let index = 0;
68
+ let prev = node.previousSibling;
69
+ while (prev) {
70
+ if (prev.nodeType === Node.TEXT_NODE)
71
+ index += 1;
72
+ prev = prev.previousSibling;
73
+ }
74
+ return index;
75
+ }
76
+ function buildElementPath(el) {
77
+ const parts = [];
78
+ let current = el;
79
+ while (current && current.tagName && current !== document.body) {
80
+ const tag = current.tagName.toLowerCase();
81
+ const idx = getElementIndex(current);
82
+ parts.push(`${tag}[${idx}]`);
83
+ current = current.parentElement;
84
+ }
85
+ parts.push("body");
86
+ return parts.reverse().join("/");
87
+ }
88
+ function isTranslatableText(text) {
89
+ if (!text || text.trim().length < 2)
90
+ return false;
91
+ if (/^(__[A-Z0-9_]+__\s*)+$/.test(text))
92
+ return false;
93
+ if (/^\d+(\.\d+)?$/.test(text))
94
+ return false;
95
+ if (!/[a-zA-Z\u00C0-\u024F\u1E00-\u1EFF]/.test(text))
96
+ return false;
97
+ return true;
98
+ }
99
+ function buildStableId(el, text, textIndex) {
100
+ const key = getStableKey(el);
101
+ const path = buildElementPath(el);
102
+ const raw = `${path}#text[${textIndex}]|${text.trim()}|${key}`;
103
+ return hashContent(raw);
104
+ }
105
+ function markElementDirect(el, rawText, stats) {
106
+ if (!el.hasAttribute("data-lovalingo-original")) {
107
+ el.setAttribute("data-lovalingo-original", rawText.trim());
108
+ }
109
+ if (!el.hasAttribute("data-lovalingo-id")) {
110
+ el.setAttribute("data-lovalingo-id", buildStableId(el, rawText, 0));
111
+ }
112
+ el.setAttribute("data-lovalingo-kind", "text");
113
+ stats.markedNodes += 1;
114
+ stats.markedChars += rawText.length;
115
+ }
116
+ function markTextNode(node, stats) {
117
+ const raw = node.nodeValue || "";
118
+ if (!raw)
119
+ return;
120
+ const trimmed = raw.trim();
121
+ if (!trimmed)
122
+ return;
123
+ stats.totalTextNodes += 1;
124
+ stats.totalChars += raw.length;
125
+ const parent = node.parentElement;
126
+ if (!parent)
127
+ return;
128
+ if (isExcludedElement(parent)) {
129
+ stats.skippedExcludedNodes += 1;
130
+ stats.skippedExcludedChars += raw.length;
131
+ return;
132
+ }
133
+ const unsafe = findUnsafeContainer(parent);
134
+ if (unsafe) {
135
+ stats.skippedUnsafeNodes += 1;
136
+ stats.skippedUnsafeChars += raw.length;
137
+ if (!unsafe.hasAttribute("data-lovalingo-unsafe")) {
138
+ unsafe.setAttribute("data-lovalingo-unsafe", unsafe.tagName.toLowerCase());
139
+ }
140
+ return;
141
+ }
142
+ if (!isTranslatableText(trimmed)) {
143
+ stats.skippedNonTranslatableNodes += 1;
144
+ stats.skippedNonTranslatableChars += raw.length;
145
+ return;
146
+ }
147
+ const existingMarker = parent.closest(MARKER_SELECTOR);
148
+ if (existingMarker) {
149
+ if (!existingMarker.getAttribute("data-lovalingo-id")) {
150
+ const textIndex = getTextNodeIndex(node);
151
+ existingMarker.setAttribute("data-lovalingo-id", buildStableId(parent, raw, textIndex));
152
+ }
153
+ stats.markedNodes += 1;
154
+ stats.markedChars += raw.length;
155
+ return;
156
+ }
157
+ const parentTag = parent.tagName.toLowerCase();
158
+ if (DIRECT_MARK_TAGS.has(parentTag)) {
159
+ markElementDirect(parent, raw, stats);
160
+ return;
161
+ }
162
+ const wrapper = document.createElement("span");
163
+ wrapper.setAttribute("data-lovalingo-original", trimmed);
164
+ wrapper.setAttribute("data-lovalingo-id", buildStableId(parent, raw, getTextNodeIndex(node)));
165
+ wrapper.setAttribute("data-lovalingo-kind", "text");
166
+ wrapper.setAttribute("data-lovalingo-marker", "1");
167
+ wrapper.textContent = raw;
168
+ try {
169
+ parent.replaceChild(wrapper, node);
170
+ }
171
+ catch {
172
+ return;
173
+ }
174
+ stats.markedNodes += 1;
175
+ stats.markedChars += raw.length;
176
+ }
177
+ function markAttributes(root) {
178
+ const nodes = root.querySelectorAll("[title],[aria-label],[placeholder]");
179
+ nodes.forEach((el) => {
180
+ if (isExcludedElement(el))
181
+ return;
182
+ if (findUnsafeContainer(el))
183
+ return;
184
+ for (const { attr, marker } of ATTRIBUTE_MARKS) {
185
+ if (el.hasAttribute(marker))
186
+ continue;
187
+ const value = el.getAttribute(attr);
188
+ if (!value)
189
+ continue;
190
+ const trimmed = value.trim();
191
+ if (!trimmed || !isTranslatableText(trimmed))
192
+ continue;
193
+ el.setAttribute(marker, trimmed);
194
+ }
195
+ });
196
+ }
197
+ function finalizeStats(stats) {
198
+ const eligibleNodes = stats.totalTextNodes -
199
+ stats.skippedUnsafeNodes -
200
+ stats.skippedExcludedNodes -
201
+ stats.skippedNonTranslatableNodes;
202
+ const eligibleChars = stats.totalChars -
203
+ stats.skippedUnsafeChars -
204
+ stats.skippedExcludedChars -
205
+ stats.skippedNonTranslatableChars;
206
+ stats.coverageRatio = eligibleNodes > 0 ? stats.markedNodes / eligibleNodes : 1;
207
+ stats.coverageRatioChars = eligibleChars > 0 ? stats.markedChars / eligibleChars : 1;
208
+ }
209
+ function scanAndMark() {
210
+ if (!running)
211
+ return;
212
+ const root = document.body;
213
+ if (!root) {
214
+ setGlobalStats(buildEmptyStats());
215
+ return;
216
+ }
217
+ const stats = buildEmptyStats();
218
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
219
+ const nodes = [];
220
+ let node = walker.nextNode();
221
+ while (node) {
222
+ if (node.nodeType === Node.TEXT_NODE)
223
+ nodes.push(node);
224
+ node = walker.nextNode();
225
+ }
226
+ nodes.forEach((textNode) => markTextNode(textNode, stats));
227
+ markAttributes(root);
228
+ finalizeStats(stats);
229
+ setGlobalStats(stats);
230
+ }
231
+ function scheduleScan() {
232
+ if (!running)
233
+ return;
234
+ if (scheduled != null)
235
+ return;
236
+ scheduled = window.setTimeout(() => {
237
+ scheduled = null;
238
+ scanAndMark();
239
+ }, throttleMs);
240
+ }
241
+ export function startMarkerEngine(options = {}) {
242
+ if (typeof window === "undefined" || typeof document === "undefined") {
243
+ return () => undefined;
244
+ }
245
+ stopMarkerEngine();
246
+ running = true;
247
+ throttleMs = Math.max(20, options.throttleMs ?? DEFAULT_THROTTLE_MS);
248
+ const startObserver = () => {
249
+ if (!running)
250
+ return;
251
+ if (!document.body) {
252
+ window.setTimeout(startObserver, 50);
253
+ return;
254
+ }
255
+ observer = new MutationObserver(() => scheduleScan());
256
+ observer.observe(document.body, {
257
+ childList: true,
258
+ subtree: true,
259
+ characterData: true,
260
+ });
261
+ scanAndMark();
262
+ };
263
+ startObserver();
264
+ return stopMarkerEngine;
265
+ }
266
+ export function stopMarkerEngine() {
267
+ running = false;
268
+ if (scheduled != null) {
269
+ window.clearTimeout(scheduled);
270
+ scheduled = null;
271
+ }
272
+ if (observer) {
273
+ observer.disconnect();
274
+ observer = null;
275
+ }
276
+ }
277
+ export function getMarkerStats() {
278
+ return lastStats;
279
+ }
@@ -8,6 +8,7 @@
8
8
  * /dashboard/projects/4458eb10-608c-4622-a92e-ec3ed2eeb524/setup
9
9
  * → /dashboard/projects/:id/setup
10
10
  */
11
+ import { warnDebug } from "./logger";
11
12
  // Common patterns for dynamic segments
12
13
  const UUID_PATTERN = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/gi;
13
14
  const NUMERIC_ID_PATTERN = /\/\d+(?=\/|$)/g;
@@ -40,7 +41,7 @@ export function normalizePath(path, config) {
40
41
  }
41
42
  }
42
43
  catch (error) {
43
- console.warn('[PathNormalizer] Invalid pattern:', rule.pattern, error);
44
+ warnDebug('[PathNormalizer] Invalid pattern:', rule.pattern, error);
44
45
  }
45
46
  }
46
47
  }
@@ -1,9 +1,11 @@
1
+ import { logDebug, warnDebug, errorDebug } from './logger';
1
2
  export class Translator {
2
3
  constructor() {
3
4
  this.translationMap = new Map();
4
5
  this.exclusions = [];
5
6
  // Brand names that should NEVER be translated
6
7
  this.nonTranslatableTerms = new Set([
8
+ 'Lovalingo',
7
9
  'Lovable', 'v0', 'Claude Code', 'Bolt', 'Base44',
8
10
  'Replit', 'GitHub', 'Supabase', 'OpenAI', 'Anthropic'
9
11
  ]);
@@ -112,7 +114,7 @@ export class Translator {
112
114
  setTranslations(translations) {
113
115
  this.translationMap.clear();
114
116
  if (!Array.isArray(translations)) {
115
- console.error('[Lovalingo] setTranslations expected array, got:', typeof translations);
117
+ errorDebug('[Lovalingo] setTranslations expected array, got:', typeof translations);
116
118
  return;
117
119
  }
118
120
  translations.forEach(t => {
@@ -121,7 +123,7 @@ export class Translator {
121
123
  this.translationMap.set(t.source_text.trim(), t.translated_text);
122
124
  }
123
125
  });
124
- console.log(`[Lovalingo] ✅ Loaded ${this.translationMap.size} translations`);
126
+ logDebug(`[Lovalingo] ✅ Loaded ${this.translationMap.size} translations`);
125
127
  }
126
128
  setExclusions(exclusions) {
127
129
  this.exclusions = exclusions || [];
@@ -333,7 +335,7 @@ export class Translator {
333
335
  const hadLettersInRaw = /[a-zA-Z\u00C0-\u024F\u1E00-\u1EFF]/.test(rawHTML);
334
336
  const hadPlaceholders = Object.keys(placeholderMap).length > 0;
335
337
  if (hadLettersInRaw || hadPlaceholders) {
336
- console.warn('[Lovalingo] ⚠️ Non-translatable content after extraction:', semanticElement.outerHTML.substring(0, 140));
338
+ warnDebug('[Lovalingo] ⚠️ Non-translatable content after extraction:', semanticElement.outerHTML.substring(0, 140));
337
339
  }
338
340
  return {
339
341
  rawHTML,
@@ -342,7 +344,7 @@ export class Translator {
342
344
  semanticContext
343
345
  };
344
346
  }
345
- console.log(`[Lovalingo] 📝 Extracted: "${processedText.substring(0, 80)}..." with ${Object.keys(placeholderMap).length} placeholders`);
347
+ logDebug(`[Lovalingo] 📝 Extracted: "${processedText.substring(0, 80)}..." with ${Object.keys(placeholderMap).length} placeholders`);
346
348
  return {
347
349
  rawHTML,
348
350
  processedText,
@@ -393,7 +395,7 @@ export class Translator {
393
395
  if (SEMANTIC_BOUNDARIES.has(element.tagName)) {
394
396
  const originalHTML = element.getAttribute('data-Lovalingo-original-html');
395
397
  if (originalHTML) {
396
- console.log(`[Lovalingo] 🔄 Restoring: "${element.textContent?.substring(0, 50)}..."`);
398
+ logDebug(`[Lovalingo] 🔄 Restoring: "${element.textContent?.substring(0, 50)}..."`);
397
399
  // Mark current live elements before restoring
398
400
  const markedElements = this.markElements(element);
399
401
  // Use safe update that preserves live elements
@@ -421,11 +423,11 @@ export class Translator {
421
423
  * Restore all elements to original state
422
424
  */
423
425
  restoreDOM() {
424
- console.log('[Lovalingo] 🔄 Restoring DOM to original state...');
426
+ logDebug('[Lovalingo] 🔄 Restoring DOM to original state...');
425
427
  const startTime = performance.now();
426
428
  this.restoreElement(document.body);
427
429
  const elapsed = performance.now() - startTime;
428
- console.log(`[Lovalingo] 🏁 DOM restoration complete in ${elapsed.toFixed(2)}ms`);
430
+ logDebug(`[Lovalingo] 🏁 DOM restoration complete in ${elapsed.toFixed(2)}ms`);
429
431
  }
430
432
  /**
431
433
  * Mark all descendant elements with unique IDs before translation
@@ -480,7 +482,7 @@ export class Translator {
480
482
  updateElementChildren(parent, newHTML, markedElements) {
481
483
  // Safety check: Ensure parent is still in the DOM
482
484
  if (!parent.isConnected || !document.body.contains(parent)) {
483
- console.warn('[Lovalingo] Parent element not in DOM, skipping translation');
485
+ warnDebug('[Lovalingo] Parent element not in DOM, skipping translation');
484
486
  return;
485
487
  }
486
488
  try {
@@ -497,7 +499,7 @@ export class Translator {
497
499
  }
498
500
  catch (e) {
499
501
  // Node might have been removed by React already, skip
500
- console.warn('[Lovalingo] Could not remove child, likely already removed by framework');
502
+ warnDebug('[Lovalingo] Could not remove child, likely already removed by framework');
501
503
  break;
502
504
  }
503
505
  }
@@ -507,13 +509,13 @@ export class Translator {
507
509
  parent.appendChild(temp.firstChild);
508
510
  }
509
511
  catch (e) {
510
- console.warn('[Lovalingo] Could not append child:', e);
512
+ warnDebug('[Lovalingo] Could not append child:', e);
511
513
  break;
512
514
  }
513
515
  }
514
516
  }
515
517
  catch (error) {
516
- console.error('[Lovalingo] Error updating element children:', error);
518
+ errorDebug('[Lovalingo] Error updating element children:', error);
517
519
  }
518
520
  }
519
521
  /**
@@ -594,7 +596,7 @@ export class Translator {
594
596
  const sourceText = originalTextAttr || content.processedText;
595
597
  // If boundary isn't translatable, recurse to children.
596
598
  if (!this.isTranslatableText(sourceText)) {
597
- console.log('[Lovalingo] ⏭️ Skipping non-translatable content:', sourceText.substring(0, 50));
599
+ logDebug('[Lovalingo] ⏭️ Skipping non-translatable content:', sourceText.substring(0, 50));
598
600
  Array.from(element.children).forEach((child) => {
599
601
  if (child instanceof HTMLElement)
600
602
  this.translateElement(child);
@@ -603,10 +605,10 @@ export class Translator {
603
605
  }
604
606
  const translated = this.translationMap.get(sourceText);
605
607
  if (translated) {
606
- console.log(`[Lovalingo] ✅ Translating: "${sourceText.substring(0, 50)}..." → "${translated.substring(0, 50)}..."`);
608
+ logDebug(`[Lovalingo] ✅ Translating: "${sourceText.substring(0, 50)}..." → "${translated.substring(0, 50)}..."`);
607
609
  let reconstructedHTML = this.reconstructHTML(translated, content.placeholderMap);
608
610
  if (/__[A-Z0-9_]+__/.test(reconstructedHTML)) {
609
- console.warn('[Lovalingo] ⚠️ Tokens remain after reconstruction, using original HTML');
611
+ warnDebug('[Lovalingo] ⚠️ Tokens remain after reconstruction, using original HTML');
610
612
  reconstructedHTML = content.rawHTML;
611
613
  }
612
614
  // Mark for MutationObserver suppression (prevents feedback loops on DOM updates).
@@ -624,7 +626,7 @@ export class Translator {
624
626
  });
625
627
  }
626
628
  else {
627
- console.log(`[Lovalingo] ❌ Miss: "${sourceText.substring(0, 80)}..."`);
629
+ logDebug(`[Lovalingo] ❌ Miss: "${sourceText.substring(0, 80)}..."`);
628
630
  }
629
631
  }
630
632
  finally {
@@ -660,15 +662,15 @@ export class Translator {
660
662
  // Use stored original text
661
663
  const sourceText = originalTextAttr || content.processedText;
662
664
  if (!this.isTranslatableText(sourceText)) {
663
- console.log('[Lovalingo] ⏭️ Skipping non-translatable content (generic):', sourceText.substring(0, 50));
665
+ logDebug('[Lovalingo] ⏭️ Skipping non-translatable content (generic):', sourceText.substring(0, 50));
664
666
  return;
665
667
  }
666
668
  const translated = this.translationMap.get(sourceText);
667
669
  if (translated) {
668
- console.log(`[Lovalingo] ✅ Generic semantic: "${sourceText.substring(0, 50)}..." → "${translated.substring(0, 50)}..."`);
670
+ logDebug(`[Lovalingo] ✅ Generic semantic: "${sourceText.substring(0, 50)}..." → "${translated.substring(0, 50)}..."`);
669
671
  let reconstructedHTML = this.reconstructHTML(translated, content.placeholderMap);
670
672
  if (/__[A-Z0-9_]+__/.test(reconstructedHTML)) {
671
- console.warn('[Lovalingo] ⚠️ Tokens remain (generic), using original HTML');
673
+ warnDebug('[Lovalingo] ⚠️ Tokens remain (generic), using original HTML');
672
674
  reconstructedHTML = content.rawHTML;
673
675
  }
674
676
  this.updateElementChildren(element, reconstructedHTML, markedElements);
@@ -680,7 +682,7 @@ export class Translator {
680
682
  });
681
683
  }
682
684
  else {
683
- console.log(`[Lovalingo] ❌ Miss (generic): "${sourceText.substring(0, 80)}..."`);
685
+ logDebug(`[Lovalingo] ❌ Miss (generic): "${sourceText.substring(0, 80)}..."`);
684
686
  }
685
687
  }
686
688
  finally {
@@ -791,10 +793,10 @@ export class Translator {
791
793
  }
792
794
  }
793
795
  translateDOM() {
794
- console.log(`[Lovalingo] 🔄 translateDOM() called with ${this.translationMap.size} translations`);
796
+ logDebug(`[Lovalingo] 🔄 translateDOM() called with ${this.translationMap.size} translations`);
795
797
  const startTime = performance.now();
796
798
  this.translateElement(document.body);
797
799
  const elapsed = performance.now() - startTime;
798
- console.log(`[Lovalingo] 🏁 translateDOM() complete in ${elapsed.toFixed(2)}ms.`);
800
+ logDebug(`[Lovalingo] 🏁 translateDOM() complete in ${elapsed.toFixed(2)}ms.`);
799
801
  }
800
802
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.0.26";
1
+ export declare const VERSION = "0.1.1";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.0.26";
1
+ export const VERSION = "0.1.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovalingo/lovalingo",
3
- "version": "0.0.29",
3
+ "version": "0.1.1",
4
4
  "description": "React translation runtime with i18n routing, deterministic bundles + DOM rules, and zero-flash rendering.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",