@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.
@@ -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
@@ -5,6 +5,7 @@ export interface LovalingoConfig {
5
5
  locales: string[];
6
6
  apiBase?: string;
7
7
  routing?: 'query' | 'path';
8
+ autoPrefixLinks?: boolean;
8
9
  switcherPosition?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
9
10
  switcherOffsetY?: number;
10
11
  editMode?: boolean;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.0.16";
1
+ export declare const VERSION = "0.0.17";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.0.16";
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.16",
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",