@lovalingo/lovalingo 0.0.21 → 0.0.22

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,6 +1,8 @@
1
- # @lovalingo/lovalingo
1
+ # @lovalingo/lovalingo — Beste Weglot‑ & i18n‑Alternative für Vibe Coder, Lovable, V‑Zero, Emagant & mehr
2
2
 
3
- Seamless website translation for React and Next.js applications with **zero-flash rendering** and **automatic language routing**.
3
+ Seamless Website‑Übersetzung für React (React Router) und Next.js mit **Zero‑Flash Rendering**, **automatischem Language‑Routing** und **SEO‑Signalen (canonical + hreflang)** — gebaut für schnelle Iteration in AI‑/Vibe‑Coding Workflows.
4
+
5
+ Wenn du mit Lovable, V‑Zero, Emagant, Vite oder ähnlichen Tools shipst, ist Lovalingo der schnellste Weg zu sauberem i18n ohne “Weglot‑Overhead”.
4
6
 
5
7
  ## Installation
6
8
 
@@ -25,7 +27,7 @@ function App() {
25
27
  return (
26
28
  <LangRouter defaultLang="en" langs={['en', 'fr', 'de', 'es']} navigateRef={navigateRef}>
27
29
  <LovalingoProvider
28
- apiKey="aix_your_api_key"
30
+ publicAnonKey="aix_your_public_anon_key"
29
31
  defaultLocale="en"
30
32
  locales={['en', 'fr', 'de', 'es']}
31
33
  routing="path"
@@ -81,7 +83,7 @@ function App() {
81
83
  return (
82
84
  <BrowserRouter>
83
85
  <LovalingoProvider
84
- apiKey="aix_your_api_key"
86
+ publicAnonKey="aix_your_public_anon_key"
85
87
  defaultLocale="en"
86
88
  locales={['en', 'de', 'fr', 'es']}
87
89
  routing="query"
@@ -104,7 +106,7 @@ export default function RootLayout({ children }) {
104
106
  <html>
105
107
  <body>
106
108
  <LovalingoProvider
107
- apiKey="aix_your_api_key"
109
+ publicAnonKey="aix_your_public_anon_key"
108
110
  defaultLocale="en"
109
111
  locales={['en', 'de', 'fr', 'es']}
110
112
  >
@@ -120,22 +122,39 @@ export default function RootLayout({ children }) {
120
122
 
121
123
  ```tsx
122
124
  <LovalingoProvider
123
- apiKey="aix_xxx" // Required: Your Lovalingo API key
125
+ publicAnonKey="aix_xxx" // Required: Your Lovalingo Public Anon Key (safe to expose)
126
+ // Backwards compatible: apiKey="aix_xxx"
124
127
  defaultLocale="en" // Required: Source language
125
128
  locales={['en', 'de', 'fr']} // Required: Supported languages
126
129
  apiBase="https://..." // Optional: Custom API endpoint
127
130
  routing="query" // Optional: 'query' | 'path' (default: 'query')
131
+ autoPrefixLinks={true} // Optional (path mode): keep locale when the app renders absolute links like "/pricing"
128
132
  switcherPosition="bottom-right" // Optional: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
129
133
  switcherOffsetY={20} // Optional: Vertical offset in pixels
130
134
  editMode={false} // Optional: Enable edit mode
131
135
  editKey="KeyE" // Optional: Keyboard shortcut for edit mode
132
136
  mode="dom" // Optional: 'dom' | 'context' (default: 'dom')
133
- sitemap={true} // Optional: Auto-inject sitemap link (default: true) - NEW in v0.0.x
137
+ pathNormalization={{ enabled: true }}// Optional: normalize paths for stable translation keys (default: enabled)
138
+ seo={true} // Optional: auto canonical + hreflang + sitemap link (default: true)
139
+ sitemap={true} // Optional: auto-inject sitemap link (default: true)
134
140
  >
135
141
  {children}
136
142
  </LovalingoProvider>
137
143
  ```
138
144
 
145
+ ### Optional: Inject via `index.html`
146
+
147
+ If your tooling prefers you to avoid hardcoding the key inside JSX, you can inject it at runtime:
148
+
149
+ ```html
150
+ <meta name="lovalingo-public-anon-key" content="aix_xxx" />
151
+ <script>
152
+ window.__LOVALINGO_PUBLIC_ANON_KEY__ = "aix_xxx";
153
+ </script>
154
+ ```
155
+
156
+ Then you may omit `publicAnonKey` and Lovalingo will read it from the meta tag or `window.__LOVALINGO_PUBLIC_ANON_KEY__`.
157
+
139
158
  ### Routing Modes
140
159
 
141
160
  - **`query`** (default): Language in query parameter `/pricing?t=fr`
@@ -146,6 +165,7 @@ export default function RootLayout({ children }) {
146
165
  - Use with `<LangRouter>` wrapper
147
166
  - Automatic language routing
148
167
  - SEO-optimized URLs
168
+ - Non-localized by default: `/auth`, `/login`, `/signup` (no `/:lang/` prefix)
149
169
  - See [PATH_EXAMPLES.md](./PATH_EXAMPLES.md) for detailed examples
150
170
 
151
171
  ## Path Mode Helpers (v0.0.x+)
@@ -259,12 +279,44 @@ function MyComponent() {
259
279
  - ✅ Zero-flash translations (rendered before browser paint)
260
280
  - ✅ Automatic route change detection
261
281
  - ✅ MutationObserver for dynamic content
282
+ - ✅ SEO: automatic `canonical` + `hreflang` (can be disabled via `seo={false}`)
262
283
  - ✅ Customizable language switcher
263
284
  - ✅ TypeScript support
264
285
  - ✅ Works with React Router and Next.js
265
286
  - ✅ Edit mode for managing exclusions
266
287
  - ✅ Automatic translation miss detection
267
288
  - ✅ **Automatic multilingual sitemap generation (v0.0.x+)**
289
+ - ✅ Path Mode helpers: `<LangRouter>`, `<LangLink>`, `useLang()`, `useLangNavigate()`
290
+ - ✅ Path Mode safety: `autoPrefixLinks` keeps users in the current language
291
+ - ✅ RTL ready: automatically sets `<html dir="rtl">` for `ar/he/fa/ur`
292
+ - ✅ Optional Context Mode: `<AutoTranslate>` with hash-based caching for React text nodes
293
+
294
+ ## SEO (Canonical + hreflang)
295
+
296
+ Lovalingo can keep `<head>` SEO signals in sync with the active locale:
297
+
298
+ - `link[rel="canonical"]` points to the current language variant
299
+ - `link[rel="alternate"][hreflang="…"]` is generated for every locale (+ `x-default`)
300
+
301
+ Disable if you already manage this elsewhere:
302
+
303
+ ```tsx
304
+ <LovalingoProvider seo={false} ... />
305
+ ```
306
+
307
+ ## Context Mode (AutoTranslate)
308
+
309
+ If you prefer React‑native translation (instead of DOM mutation), enable `mode="context"` and wrap UI blocks with `<AutoTranslate>`:
310
+
311
+ ```tsx
312
+ import { LovalingoProvider, AutoTranslate } from "@lovalingo/lovalingo";
313
+
314
+ <LovalingoProvider mode="context" ...>
315
+ <AutoTranslate>
316
+ <App />
317
+ </AutoTranslate>
318
+ </LovalingoProvider>
319
+ ```
268
320
 
269
321
  ## Automatic Sitemap (v0.0.x+)
270
322
 
@@ -274,7 +326,7 @@ Your multilingual sitemap is **automatically generated** and should be exposed o
274
326
 
275
327
  ```tsx
276
328
  <LovalingoProvider
277
- apiKey="aix_xxx"
329
+ publicAnonKey="aix_xxx"
278
330
  defaultLocale="en"
279
331
  locales={['en', 'de', 'fr', 'es']}
280
332
  >
@@ -305,7 +357,7 @@ Your multilingual sitemap is **automatically generated** and should be exposed o
305
357
 
306
358
  ```tsx
307
359
  <LovalingoProvider
308
- apiKey="aix_xxx"
360
+ publicAnonKey="aix_xxx"
309
361
  defaultLocale="en"
310
362
  locales={['en', 'de', 'fr']}
311
363
  sitemap={false} // Disable automatic sitemap
@@ -5,27 +5,38 @@ import { Translator } from '../utils/translator';
5
5
  import { LanguageSwitcher } from './LanguageSwitcher';
6
6
  import { NavigationOverlay } from './NavigationOverlay';
7
7
  const LOCALE_STORAGE_KEY = 'Lovalingo_locale';
8
- export const LovalingoProvider = ({ children, apiKey, defaultLocale, locales, apiBase = 'https://leuskvkajliuzalrlwhw.supabase.co', routing = 'query', // Default to query mode (backward compatible)
8
+ export const LovalingoProvider = ({ children, apiKey: apiKeyProp, publicAnonKey, defaultLocale, locales, apiBase = 'https://leuskvkajliuzalrlwhw.supabase.co', routing = 'query', // Default to query mode (backward compatible)
9
9
  autoPrefixLinks = true, switcherPosition = 'bottom-right', switcherOffsetY = 20, editMode: initialEditMode = false, editKey = 'KeyE', pathNormalization = { enabled: true }, // Enable by default
10
10
  mode = 'dom', // Default to legacy DOM mode for backward compatibility
11
11
  sitemap = true, // Default: true - Auto-inject sitemap link tag
12
12
  seo = true, // Default: true - Can be disabled per project entitlements
13
13
  navigateRef, // For path mode routing
14
14
  }) => {
15
+ const metaKey = typeof document !== "undefined"
16
+ ? document.querySelector('meta[name="lovalingo-public-anon-key"]')?.content?.trim() || ""
17
+ : "";
18
+ const resolvedApiKey = (typeof apiKeyProp === "string" && apiKeyProp.trim().length > 0
19
+ ? apiKeyProp
20
+ : typeof publicAnonKey === "string" && publicAnonKey.trim().length > 0
21
+ ? publicAnonKey
22
+ : globalThis
23
+ .__LOVALINGO_PUBLIC_ANON_KEY__ ||
24
+ globalThis.__LOVALINGO_API_KEY__ ||
25
+ metaKey ||
26
+ "");
27
+ const rawLocales = Array.isArray(locales) ? locales : [];
15
28
  // Stabilize locale lists even when callers pass inline arrays (e.g. locales={["en","de"]})
16
29
  // so effects/callbacks don't re-run every render.
17
- const localesKey = Array.isArray(locales) ? locales.join(",") : "";
30
+ const localesKey = rawLocales.join(",");
18
31
  const allLocales = useMemo(() => {
19
- const base = (Array.isArray(locales) ? locales : []).includes(defaultLocale)
20
- ? (Array.isArray(locales) ? locales : [])
21
- : [defaultLocale, ...(Array.isArray(locales) ? locales : [])];
32
+ const base = rawLocales.includes(defaultLocale) ? rawLocales : [defaultLocale, ...rawLocales];
22
33
  return Array.from(new Set(base));
23
- }, [defaultLocale, localesKey]);
34
+ }, [defaultLocale, localesKey, rawLocales]);
24
35
  // Initialize locale from localStorage to prevent flash of default locale on navigation
25
36
  const [locale, setLocaleState] = useState(() => {
26
37
  try {
27
38
  const stored = localStorage.getItem(LOCALE_STORAGE_KEY);
28
- if (stored && (locales.includes(stored) || stored === defaultLocale)) {
39
+ if (stored && (allLocales.includes(stored) || stored === defaultLocale)) {
29
40
  return stored;
30
41
  }
31
42
  }
@@ -42,7 +53,7 @@ navigateRef, // For path mode routing
42
53
  const enhancedPathConfig = routing === 'path'
43
54
  ? { ...pathNormalization, supportedLocales: allLocales }
44
55
  : pathNormalization;
45
- const apiRef = useRef(new LovalingoAPI(apiKey, apiBase, enhancedPathConfig));
56
+ const apiRef = useRef(new LovalingoAPI(resolvedApiKey, apiBase, enhancedPathConfig));
46
57
  const [entitlements, setEntitlements] = useState(() => apiRef.current.getEntitlements());
47
58
  const observerRef = useRef(null);
48
59
  const missReportIntervalRef = useRef(null);
@@ -56,7 +67,8 @@ navigateRef, // For path mode routing
56
67
  const translatingHashesRef = useRef(new Set());
57
68
  const [, forceUpdate] = useState({});
58
69
  const config = {
59
- apiKey,
70
+ apiKey: resolvedApiKey,
71
+ publicAnonKey: resolvedApiKey,
60
72
  defaultLocale,
61
73
  locales: allLocales,
62
74
  apiBase,
@@ -453,7 +465,7 @@ navigateRef, // For path mode routing
453
465
  }, [detectLocale, loadData, editKey]);
454
466
  // Auto-inject sitemap link tag
455
467
  useEffect(() => {
456
- if (sitemap && apiKey && isSeoActive()) {
468
+ if (sitemap && resolvedApiKey && isSeoActive()) {
457
469
  // Prefer same-origin /sitemap.xml so crawlers discover the canonical sitemap URL.
458
470
  // Hosting should route /sitemap.xml to Lovalingo's generate-sitemap endpoint.
459
471
  const sitemapUrl = `${window.location.origin}/sitemap.xml`;
@@ -475,7 +487,7 @@ navigateRef, // For path mode routing
475
487
  }
476
488
  };
477
489
  }
478
- }, [sitemap, apiKey, apiBase, isSeoActive]);
490
+ }, [sitemap, resolvedApiKey, apiBase, isSeoActive]);
479
491
  // Watch for route changes (browser back/forward + SPA navigation)
480
492
  useEffect(() => {
481
493
  let navigationTimeout = null;
@@ -1,6 +1,14 @@
1
1
  import React from 'react';
2
2
  import { Link } from 'react-router-dom';
3
3
  import { useLang } from '../hooks/useLang';
4
+ const NON_LOCALIZED_APP_PATHS = new Set([
5
+ 'auth',
6
+ 'login',
7
+ 'signup',
8
+ 'sign-in',
9
+ 'sign-up',
10
+ 'register',
11
+ ]);
4
12
  /**
5
13
  * LangLink - Language-aware Link component
6
14
  *
@@ -22,7 +30,11 @@ export function LangLink({ to, ...props }) {
22
30
  const lang = useLang();
23
31
  // If 'to' is a string, prepend language
24
32
  const langTo = typeof to === 'string'
25
- ? `/${lang}/${to.replace(/^\//, '')}`
33
+ ? (() => {
34
+ const trimmed = to.replace(/^\//, '');
35
+ const firstSegment = trimmed.split('/')[0] || '';
36
+ return NON_LOCALIZED_APP_PATHS.has(firstSegment) ? `/${trimmed}` : `/${lang}/${trimmed}`;
37
+ })()
26
38
  : to;
27
39
  return React.createElement(Link, { ...props, to: langTo });
28
40
  }
@@ -1,6 +1,17 @@
1
1
  import React, { useEffect } from 'react';
2
- import { BrowserRouter, Routes, Route, Navigate, Outlet, useLocation, useParams, useNavigate } from 'react-router-dom';
2
+ import { BrowserRouter, Routes, Route, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
3
+ import { LangContext } from '../context/LangContext';
4
+ const NON_LOCALIZED_APP_PATHS = new Set([
5
+ '/auth',
6
+ '/login',
7
+ '/signup',
8
+ '/sign-in',
9
+ '/sign-up',
10
+ '/register',
11
+ ]);
3
12
  function isNonLocalizedPath(pathname) {
13
+ if (NON_LOCALIZED_APP_PATHS.has(pathname))
14
+ return true;
4
15
  if (pathname === '/robots.txt' || pathname === '/sitemap.xml')
5
16
  return true;
6
17
  if (pathname.startsWith('/.well-known/'))
@@ -22,18 +33,19 @@ function NavigateExporter({ navigateRef }) {
22
33
  /**
23
34
  * LangGuard - Internal component that validates language and provides it to children
24
35
  */
25
- function LangGuard({ defaultLang, langs }) {
26
- const { lang } = useParams();
36
+ function LangGuard({ defaultLang, lang }) {
27
37
  const location = useLocation();
28
- // If invalid language, redirect to default
29
- if (!lang || !langs.includes(lang)) {
30
- // Treat missing/invalid lang as a URL without locale prefix, and preserve the full path.
31
- // Example: /projects/x -> /en/projects/x
32
- const nextPath = `/${defaultLang}${location.pathname}${location.search}${location.hash}`;
38
+ // If the URL is language-prefixed but the underlying route is non-localized (auth/login/signup),
39
+ // redirect to the canonical non-localized path.
40
+ const prefix = `/${lang}`;
41
+ const restPath = location.pathname.startsWith(prefix) ? location.pathname.slice(prefix.length) || '/' : location.pathname;
42
+ if (isNonLocalizedPath(restPath)) {
43
+ const nextPath = `${restPath}${location.search}${location.hash}`;
33
44
  return React.createElement(Navigate, { to: nextPath, replace: true });
34
45
  }
35
46
  // Valid language - render children (user's routes)
36
- return React.createElement(Outlet, { context: { lang } });
47
+ return (React.createElement(LangContext.Provider, { value: lang },
48
+ React.createElement(Outlet, { context: { lang } })));
37
49
  }
38
50
  function RedirectToDefaultLang({ defaultLang, children }) {
39
51
  const location = useLocation();
@@ -74,8 +86,8 @@ export function LangRouter({ children, defaultLang, langs, navigateRef }) {
74
86
  return (React.createElement(BrowserRouter, null,
75
87
  React.createElement(NavigateExporter, { navigateRef: navigateRef }),
76
88
  React.createElement(Routes, null,
77
- React.createElement(Route, { path: ":lang/*", element: React.createElement(LangGuard, { defaultLang: defaultLang, langs: langs }) },
89
+ langs.map((lang) => (React.createElement(Route, { key: lang, path: `${lang}/*`, element: React.createElement(LangGuard, { defaultLang: defaultLang, lang: lang }) },
78
90
  React.createElement(Route, { index: true, element: React.createElement(React.Fragment, null, children) }),
79
- React.createElement(Route, { path: "*", element: React.createElement(React.Fragment, null, children) })),
91
+ React.createElement(Route, { path: "*", element: React.createElement(React.Fragment, null, children) })))),
80
92
  React.createElement(Route, { path: "*", element: React.createElement(RedirectToDefaultLang, { defaultLang: defaultLang }, children) }))));
81
93
  }
@@ -0,0 +1 @@
1
+ export declare const LangContext: import("react").Context<string | null>;
@@ -0,0 +1,2 @@
1
+ import { createContext } from "react";
2
+ export const LangContext = createContext(null);
@@ -1,4 +1,6 @@
1
1
  import { useParams } from 'react-router-dom';
2
+ import { useContext } from 'react';
3
+ import { LangContext } from '../context/LangContext';
2
4
  /**
3
5
  * useLang - Get the current language from the URL
4
6
  *
@@ -15,6 +17,7 @@ import { useParams } from 'react-router-dom';
15
17
  * @returns Current language code (e.g., 'en', 'fr', 'de')
16
18
  */
17
19
  export function useLang() {
20
+ const ctxLang = useContext(LangContext);
18
21
  const { lang } = useParams();
19
- return lang ?? 'en'; // Fallback to 'en' if somehow not found
22
+ return ctxLang ?? lang ?? 'en'; // Fallback to 'en' if not found
20
23
  }
@@ -20,5 +20,5 @@
20
20
  */
21
21
  export declare function useLangNavigate(): (path: string, options?: {
22
22
  replace?: boolean;
23
- state?: any;
23
+ state?: unknown;
24
24
  }) => void;
@@ -1,5 +1,14 @@
1
1
  import { useNavigate } from 'react-router-dom';
2
+ import { useCallback } from 'react';
2
3
  import { useLang } from './useLang';
4
+ const NON_LOCALIZED_APP_PATHS = new Set([
5
+ 'auth',
6
+ 'login',
7
+ 'signup',
8
+ 'sign-in',
9
+ 'sign-up',
10
+ 'register',
11
+ ]);
3
12
  /**
4
13
  * useLangNavigate - Get a language-aware navigate function
5
14
  *
@@ -23,8 +32,12 @@ import { useLang } from './useLang';
23
32
  export function useLangNavigate() {
24
33
  const navigate = useNavigate();
25
34
  const lang = useLang();
26
- return (path, options) => {
27
- const fullPath = `/${lang}/${path.replace(/^\//, '')}`;
35
+ return useCallback((path, options) => {
36
+ const trimmed = path.replace(/^\//, '');
37
+ const firstSegment = trimmed.split('/')[0] || '';
38
+ const fullPath = NON_LOCALIZED_APP_PATHS.has(firstSegment)
39
+ ? `/${trimmed}`
40
+ : `/${lang}/${trimmed}`;
28
41
  navigate(fullPath, options);
29
- };
42
+ }, [lang, navigate]);
30
43
  }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,15 @@
1
1
  import { PathNormalizationConfig } from './utils/pathNormalizer';
2
2
  export interface LovalingoConfig {
3
- apiKey: string;
3
+ /**
4
+ * Public project key (safe to expose in the browser).
5
+ * Backwards compatible alias: you can still pass `apiKey`.
6
+ */
7
+ publicAnonKey?: string;
8
+ /**
9
+ * Backwards compatible name for the public project key.
10
+ * Prefer `publicAnonKey` in new installs.
11
+ */
12
+ apiKey?: string;
4
13
  defaultLocale: string;
5
14
  locales: string[];
6
15
  apiBase?: string;
@@ -16,6 +16,7 @@ export declare class LovalingoAPI {
16
16
  constructor(apiKey: string, apiBase: string, pathConfig?: PathNormalizationConfig);
17
17
  private hasApiKey;
18
18
  private warnMissingApiKey;
19
+ private logActivationRequired;
19
20
  getEntitlements(): ProjectEntitlements | null;
20
21
  fetchEntitlements(localeHint: string): Promise<ProjectEntitlements | null>;
21
22
  fetchTranslations(sourceLocale: string, targetLocale: string): Promise<Translation[]>;
package/dist/utils/api.js CHANGED
@@ -11,7 +11,13 @@ export class LovalingoAPI {
11
11
  }
12
12
  warnMissingApiKey(action) {
13
13
  // Avoid hard-crashing apps; make the failure mode obvious.
14
- console.warn(`[Lovalingo] Missing apiKey: ${action} was skipped. Pass apiKey to <LovalingoProvider apiKey="..."> (or set VITE_LOVALINGO_API_KEY).`);
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
+ }
16
+ logActivationRequired(context, response) {
17
+ console.error(`[Lovalingo] ${context} blocked (HTTP ${response.status}). ` +
18
+ `This project is not activated yet. ` +
19
+ `Publish a public routes manifest at "/.well-known/lovalingo-routes.json" on your domain, ` +
20
+ `then verify it in the Lovalingo dashboard to activate translations + SEO.`);
15
21
  }
16
22
  getEntitlements() {
17
23
  return this.entitlements;
@@ -24,6 +30,10 @@ export class LovalingoAPI {
24
30
  }
25
31
  const normalizedPath = processPath(window.location.pathname, this.pathConfig);
26
32
  const response = await fetch(`${this.apiBase}/functions/v1/bundle?key=${this.apiKey}&locale=${localeHint}&path=${normalizedPath}`);
33
+ if (response.status === 403) {
34
+ this.logActivationRequired('fetchEntitlements', response);
35
+ return null;
36
+ }
27
37
  if (!response.ok)
28
38
  return null;
29
39
  const data = await response.json();
@@ -49,6 +59,10 @@ export class LovalingoAPI {
49
59
  // Use path normalization utility
50
60
  const normalizedPath = processPath(window.location.pathname, this.pathConfig);
51
61
  const response = await fetch(`${this.apiBase}/functions/v1/bundle?key=${this.apiKey}&locale=${targetLocale}&path=${normalizedPath}`);
62
+ if (response.status === 403) {
63
+ this.logActivationRequired('fetchTranslations', response);
64
+ return [];
65
+ }
52
66
  if (!response.ok)
53
67
  throw new Error('Failed to fetch translations');
54
68
  const data = await response.json();
@@ -81,6 +95,10 @@ export class LovalingoAPI {
81
95
  return [];
82
96
  }
83
97
  const response = await fetch(`${this.apiBase}/functions/v1/exclusions?key=${this.apiKey}`);
98
+ if (response.status === 403) {
99
+ this.logActivationRequired('fetchExclusions', response);
100
+ return [];
101
+ }
84
102
  if (!response.ok)
85
103
  throw new Error('Failed to fetch exclusions');
86
104
  const data = await response.json();
@@ -133,6 +151,10 @@ export class LovalingoAPI {
133
151
  path: normalizedPath,
134
152
  }),
135
153
  });
154
+ if (response.status === 403) {
155
+ this.logActivationRequired('reportMisses', response);
156
+ return;
157
+ }
136
158
  if (!response.ok) {
137
159
  const errorText = await response.text();
138
160
  console.error(`[Lovalingo] ❌ API Error ${response.status}:`, errorText);
@@ -151,11 +173,14 @@ export class LovalingoAPI {
151
173
  this.warnMissingApiKey('saveExclusion');
152
174
  return;
153
175
  }
154
- await fetch(`${this.apiBase}/functions/v1/exclusions?key=${this.apiKey}`, {
176
+ const response = await fetch(`${this.apiBase}/functions/v1/exclusions?key=${this.apiKey}`, {
155
177
  method: 'POST',
156
178
  headers: { 'Content-Type': 'application/json' },
157
179
  body: JSON.stringify({ selector, type }),
158
180
  });
181
+ if (response.status === 403) {
182
+ this.logActivationRequired('saveExclusion', response);
183
+ }
159
184
  }
160
185
  catch (error) {
161
186
  console.error('Error saving exclusion:', error);
@@ -186,6 +211,10 @@ export class LovalingoAPI {
186
211
  targetLocale,
187
212
  }),
188
213
  });
214
+ if (response.status === 403) {
215
+ this.logActivationRequired('translateRealtime', response);
216
+ return null;
217
+ }
189
218
  if (!response.ok) {
190
219
  const errorText = await response.text();
191
220
  console.error(`[Lovalingo] ❌ Real-time translation failed:`, errorText);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovalingo/lovalingo",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "description": "React translation library with automatic routing, real-time AI translation, and zero-flash rendering. One-line language routing setup.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",