@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 +61 -9
- package/dist/components/AixsterProvider.js +23 -11
- package/dist/components/LangLink.js +13 -1
- package/dist/components/LangRouter.js +23 -11
- package/dist/context/LangContext.d.ts +1 -0
- package/dist/context/LangContext.js +2 -0
- package/dist/hooks/useLang.js +4 -1
- package/dist/hooks/useLangNavigate.d.ts +1 -1
- package/dist/hooks/useLangNavigate.js +16 -3
- package/dist/types.d.ts +10 -1
- package/dist/utils/api.d.ts +1 -0
- package/dist/utils/api.js +31 -2
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
30
|
+
const localesKey = rawLocales.join(",");
|
|
18
31
|
const allLocales = useMemo(() => {
|
|
19
|
-
const base =
|
|
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 && (
|
|
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(
|
|
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 &&
|
|
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,
|
|
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
|
-
?
|
|
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,
|
|
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,
|
|
26
|
-
const { lang } = useParams();
|
|
36
|
+
function LangGuard({ defaultLang, lang }) {
|
|
27
37
|
const location = useLocation();
|
|
28
|
-
// If
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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(
|
|
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, {
|
|
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>;
|
package/dist/hooks/useLang.js
CHANGED
|
@@ -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
|
|
22
|
+
return ctxLang ?? lang ?? 'en'; // Fallback to 'en' if not found
|
|
20
23
|
}
|
|
@@ -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
|
|
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
|
-
|
|
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;
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|