@lovalingo/lovalingo 0.0.16 → 0.0.17
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/dist/components/AixsterProvider.js +147 -1
- package/dist/types.d.ts +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { LanguageSwitcher } from './LanguageSwitcher';
|
|
|
6
6
|
import { NavigationOverlay } from './NavigationOverlay';
|
|
7
7
|
const LOCALE_STORAGE_KEY = 'Lovalingo_locale';
|
|
8
8
|
export const LovalingoProvider = ({ children, apiKey, defaultLocale, locales, apiBase = 'https://leuskvkajliuzalrlwhw.supabase.co', routing = 'query', // Default to query mode (backward compatible)
|
|
9
|
-
switcherPosition = 'bottom-right', switcherOffsetY = 20, editMode: initialEditMode = false, editKey = 'KeyE', pathNormalization = { enabled: true }, // Enable by default
|
|
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
|
navigateRef, // For path mode routing
|
|
@@ -55,6 +55,7 @@ navigateRef, // For path mode routing
|
|
|
55
55
|
locales: allLocales,
|
|
56
56
|
apiBase,
|
|
57
57
|
routing,
|
|
58
|
+
autoPrefixLinks,
|
|
58
59
|
switcherPosition,
|
|
59
60
|
switcherOffsetY,
|
|
60
61
|
editMode: initialEditMode,
|
|
@@ -539,6 +540,151 @@ navigateRef, // For path mode routing
|
|
|
539
540
|
clearTimeout(navigationTimeout);
|
|
540
541
|
};
|
|
541
542
|
}, [locale, detectLocale, loadData, defaultLocale]);
|
|
543
|
+
// PATH mode: auto-prefix internal links that are missing a locale segment.
|
|
544
|
+
// This prevents "losing" the current locale when the app renders absolute links like "/projects/slug"
|
|
545
|
+
// while the user is on "/de/...".
|
|
546
|
+
useEffect(() => {
|
|
547
|
+
if (routing !== 'path')
|
|
548
|
+
return;
|
|
549
|
+
if (!autoPrefixLinks)
|
|
550
|
+
return;
|
|
551
|
+
const supportedLocales = allLocales;
|
|
552
|
+
const isAssetPath = (pathname) => {
|
|
553
|
+
if (pathname === '/robots.txt' || pathname === '/sitemap.xml')
|
|
554
|
+
return true;
|
|
555
|
+
if (pathname.startsWith('/.well-known/'))
|
|
556
|
+
return true;
|
|
557
|
+
return /\.(?:png|jpg|jpeg|gif|svg|webp|avif|ico|css|js|map|json|xml|txt|pdf|zip|gz|br|woff2?|ttf|eot)$/i.test(pathname);
|
|
558
|
+
};
|
|
559
|
+
const shouldProcessCurrentPath = () => {
|
|
560
|
+
const parts = window.location.pathname.split('/').filter(Boolean);
|
|
561
|
+
return parts.length > 0 && supportedLocales.includes(parts[0]);
|
|
562
|
+
};
|
|
563
|
+
const buildLocalePrefixedPath = (rawHref) => {
|
|
564
|
+
if (!rawHref)
|
|
565
|
+
return null;
|
|
566
|
+
const trimmed = rawHref.trim();
|
|
567
|
+
if (!trimmed)
|
|
568
|
+
return null;
|
|
569
|
+
// Only rewrite absolute-path or same-origin absolute URLs.
|
|
570
|
+
const isAbsolutePath = trimmed.startsWith('/');
|
|
571
|
+
const isAbsoluteUrl = /^https?:\/\//i.test(trimmed) || trimmed.startsWith('//');
|
|
572
|
+
if (!isAbsolutePath && !isAbsoluteUrl)
|
|
573
|
+
return null;
|
|
574
|
+
// Ignore special schemes / fragments
|
|
575
|
+
if (/^(?:#|mailto:|tel:|sms:|javascript:)/i.test(trimmed))
|
|
576
|
+
return null;
|
|
577
|
+
let url;
|
|
578
|
+
try {
|
|
579
|
+
url = new URL(trimmed, window.location.origin);
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
if (url.origin !== window.location.origin)
|
|
585
|
+
return null;
|
|
586
|
+
if (isAssetPath(url.pathname))
|
|
587
|
+
return null;
|
|
588
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
589
|
+
if (parts.length === 0)
|
|
590
|
+
return null;
|
|
591
|
+
if (supportedLocales.includes(parts[0]))
|
|
592
|
+
return null; // already locale-prefixed
|
|
593
|
+
const nextPathname = `/${locale}${url.pathname.startsWith('/') ? '' : '/'}${url.pathname.replace(/^\//, '')}`;
|
|
594
|
+
return `${nextPathname}${url.search}${url.hash}`;
|
|
595
|
+
};
|
|
596
|
+
const ORIGINAL_HREF_KEY = 'data-Lovalingo-href-original';
|
|
597
|
+
const patchAnchor = (a) => {
|
|
598
|
+
if (!a || a.hasAttribute('data-Lovalingo-exclude'))
|
|
599
|
+
return;
|
|
600
|
+
const original = a.getAttribute(ORIGINAL_HREF_KEY) ?? a.getAttribute('href') ?? '';
|
|
601
|
+
if (!a.getAttribute(ORIGINAL_HREF_KEY) && original) {
|
|
602
|
+
a.setAttribute(ORIGINAL_HREF_KEY, original);
|
|
603
|
+
}
|
|
604
|
+
const fixed = buildLocalePrefixedPath(original);
|
|
605
|
+
if (fixed) {
|
|
606
|
+
if (a.getAttribute('href') !== fixed)
|
|
607
|
+
a.setAttribute('href', fixed);
|
|
608
|
+
}
|
|
609
|
+
else if (original) {
|
|
610
|
+
// If we previously rewrote it, restore the original when it no longer applies.
|
|
611
|
+
if (a.getAttribute('href') !== original)
|
|
612
|
+
a.setAttribute('href', original);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
const patchAllAnchors = () => {
|
|
616
|
+
if (!shouldProcessCurrentPath())
|
|
617
|
+
return;
|
|
618
|
+
document.querySelectorAll('a[href]').forEach((node) => {
|
|
619
|
+
if (node instanceof HTMLAnchorElement)
|
|
620
|
+
patchAnchor(node);
|
|
621
|
+
});
|
|
622
|
+
};
|
|
623
|
+
// Patch existing anchors (also updates when locale changes)
|
|
624
|
+
patchAllAnchors();
|
|
625
|
+
// Patch new anchors when the DOM changes
|
|
626
|
+
const mo = new MutationObserver((mutations) => {
|
|
627
|
+
if (!shouldProcessCurrentPath())
|
|
628
|
+
return;
|
|
629
|
+
for (const mutation of mutations) {
|
|
630
|
+
mutation.addedNodes.forEach((node) => {
|
|
631
|
+
if (!(node instanceof HTMLElement))
|
|
632
|
+
return;
|
|
633
|
+
if (node instanceof HTMLAnchorElement) {
|
|
634
|
+
patchAnchor(node);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
node.querySelectorAll?.('a[href]').forEach((a) => {
|
|
638
|
+
if (a instanceof HTMLAnchorElement)
|
|
639
|
+
patchAnchor(a);
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
mo.observe(document.body, { childList: true, subtree: true });
|
|
645
|
+
// Click interception (capture) to handle cases where frameworks (e.g. React Router <Link>)
|
|
646
|
+
// navigate based on their "to" prop rather than the DOM href attribute.
|
|
647
|
+
const onClickCapture = (event) => {
|
|
648
|
+
if (!shouldProcessCurrentPath())
|
|
649
|
+
return;
|
|
650
|
+
if (event.defaultPrevented)
|
|
651
|
+
return;
|
|
652
|
+
if (event.button !== 0)
|
|
653
|
+
return;
|
|
654
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
655
|
+
return;
|
|
656
|
+
const target = event.target;
|
|
657
|
+
const a = target?.closest?.('a[href]');
|
|
658
|
+
if (!a)
|
|
659
|
+
return;
|
|
660
|
+
// Let the browser handle new tabs/downloads/etc.
|
|
661
|
+
if (a.target && a.target !== '_self')
|
|
662
|
+
return;
|
|
663
|
+
if (a.hasAttribute('download'))
|
|
664
|
+
return;
|
|
665
|
+
if (a.getAttribute('rel')?.includes('external'))
|
|
666
|
+
return;
|
|
667
|
+
const original = a.getAttribute(ORIGINAL_HREF_KEY) ?? a.getAttribute('href') ?? '';
|
|
668
|
+
const fixed = buildLocalePrefixedPath(original);
|
|
669
|
+
if (!fixed)
|
|
670
|
+
return;
|
|
671
|
+
event.preventDefault();
|
|
672
|
+
event.stopImmediatePropagation?.();
|
|
673
|
+
event.stopPropagation();
|
|
674
|
+
const navigate = navigateRef?.current;
|
|
675
|
+
if (navigate) {
|
|
676
|
+
navigate(fixed);
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
window.location.assign(fixed);
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
document.addEventListener('click', onClickCapture, true);
|
|
683
|
+
return () => {
|
|
684
|
+
mo.disconnect();
|
|
685
|
+
document.removeEventListener('click', onClickCapture, true);
|
|
686
|
+
};
|
|
687
|
+
}, [routing, autoPrefixLinks, allLocales, locale, navigateRef]);
|
|
542
688
|
// Set up MutationObserver for dynamic content (DOM mode only)
|
|
543
689
|
useEffect(() => {
|
|
544
690
|
if (mode !== 'dom')
|
package/dist/types.d.ts
CHANGED
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.0.
|
|
1
|
+
export declare const VERSION = "0.0.17";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.0.
|
|
1
|
+
export const VERSION = "0.0.17";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovalingo/lovalingo",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
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",
|