@lovalingo/lovalingo 0.0.13 → 0.0.15

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.
@@ -42,6 +42,7 @@ navigateRef, // For path mode routing
42
42
  const missReportIntervalRef = useRef(null);
43
43
  const retryTimeoutRef = useRef(null);
44
44
  const isNavigatingRef = useRef(false);
45
+ const isInternalNavigationRef = useRef(false);
45
46
  const translationCacheRef = useRef(new Map());
46
47
  const exclusionsCacheRef = useRef(null);
47
48
  // NEW: Hash-based translation cache for React Context system
@@ -306,43 +307,60 @@ navigateRef, // For path mode routing
306
307
  }, [defaultLocale]);
307
308
  // Change locale
308
309
  const setLocale = useCallback((newLocale) => {
309
- if (!allLocales.includes(newLocale))
310
- return;
311
- const previousLocale = locale; // Capture current locale before switching
312
- // Save to localStorage
313
- try {
314
- localStorage.setItem(LOCALE_STORAGE_KEY, newLocale);
315
- }
316
- catch (e) {
317
- console.warn('Failed to save locale to localStorage:', e);
318
- }
319
- // Update URL based on routing strategy
320
- if (routing === 'path') {
321
- // Path mode: use React Router navigate
322
- const navigate = navigateRef?.current;
323
- if (!navigate) {
324
- console.error('PATH mode requires navigateRef prop. Pass navigateRef to both <LangRouter> and <LovalingoProvider>');
310
+ void (async () => {
311
+ if (!allLocales.includes(newLocale))
325
312
  return;
313
+ const previousLocale = locale; // Capture current locale before switching
314
+ // Save to localStorage
315
+ try {
316
+ localStorage.setItem(LOCALE_STORAGE_KEY, newLocale);
326
317
  }
327
- const pathParts = window.location.pathname.split('/').filter(Boolean);
328
- // Strip existing locale
329
- if (allLocales.includes(pathParts[0])) {
330
- pathParts.shift();
318
+ catch (e) {
319
+ console.warn('Failed to save locale to localStorage:', e);
331
320
  }
332
- // Build new path with new locale
333
- const basePath = pathParts.join('/');
334
- const newPath = `/${newLocale}${basePath ? '/' + basePath : ''}${window.location.search}${window.location.hash}`;
335
- navigate(newPath);
336
- }
337
- else if (routing === 'query') {
338
- // Query mode: update query parameter (/pricing?t=en -> /pricing?t=fr)
339
- const url = new URL(window.location.href);
340
- url.searchParams.set('t', newLocale);
341
- window.history.pushState({}, '', url.toString());
342
- }
343
- setLocaleState(newLocale);
344
- loadData(newLocale, previousLocale); // Pass previous locale for restoration
345
- }, [allLocales, locale, routing, loadData]);
321
+ isInternalNavigationRef.current = true;
322
+ // Show navigation overlay immediately (only when a non-default locale is involved)
323
+ if (locale !== defaultLocale || newLocale !== defaultLocale) {
324
+ setIsNavigationLoading(true);
325
+ }
326
+ // Prevent MutationObserver work during the switch to avoid React conflicts
327
+ isNavigatingRef.current = true;
328
+ // Update URL based on routing strategy
329
+ if (routing === 'path') {
330
+ const pathParts = window.location.pathname.split('/').filter(Boolean);
331
+ // Strip existing locale
332
+ if (allLocales.includes(pathParts[0])) {
333
+ pathParts.shift();
334
+ }
335
+ // Build new path with new locale
336
+ const basePath = pathParts.join('/');
337
+ const newPath = `/${newLocale}${basePath ? '/' + basePath : ''}${window.location.search}${window.location.hash}`;
338
+ // Prefer React Router navigation when available, but gracefully fallback for non-React-Router apps
339
+ const navigate = navigateRef?.current;
340
+ if (navigate) {
341
+ navigate(newPath);
342
+ }
343
+ else {
344
+ try {
345
+ window.history.pushState({}, '', newPath);
346
+ }
347
+ catch {
348
+ window.location.assign(newPath);
349
+ }
350
+ }
351
+ }
352
+ else if (routing === 'query') {
353
+ const url = new URL(window.location.href);
354
+ url.searchParams.set('t', newLocale);
355
+ window.history.pushState({}, '', url.toString());
356
+ }
357
+ setLocaleState(newLocale);
358
+ // Always load data with overlay when switching locales to provide feedback and ensure cleanup
359
+ await loadData(newLocale, previousLocale, true);
360
+ })().finally(() => {
361
+ isInternalNavigationRef.current = false;
362
+ });
363
+ }, [allLocales, locale, routing, loadData, defaultLocale, navigateRef]);
346
364
  // NEW: Get translation by hash (for React Context system)
347
365
  const getTranslation = useCallback((hash, fallback) => {
348
366
  return hashTranslations.get(hash) || null;
@@ -442,17 +460,16 @@ navigateRef, // For path mode routing
442
460
  useEffect(() => {
443
461
  let navigationTimeout = null;
444
462
  const handlePopState = () => {
463
+ if (isInternalNavigationRef.current)
464
+ return;
445
465
  const newLocale = detectLocale();
446
466
  const previousLocale = locale;
447
467
  // Show navigation overlay immediately
448
468
  if (locale !== defaultLocale || newLocale !== defaultLocale) {
449
469
  setIsNavigationLoading(true);
450
470
  }
451
- // SET NAVIGATION FLAG & DISCONNECT OBSERVER
471
+ // SET NAVIGATION FLAG (MutationObserver callback will ignore while navigating)
452
472
  isNavigatingRef.current = true;
453
- if (observerRef.current) {
454
- observerRef.current.disconnect();
455
- }
456
473
  if (newLocale !== locale) {
457
474
  setLocaleState(newLocale);
458
475
  // Clear any pending navigation
@@ -477,17 +494,16 @@ navigateRef, // For path mode routing
477
494
  const originalPushState = history.pushState;
478
495
  const originalReplaceState = history.replaceState;
479
496
  const handleNavigation = () => {
497
+ if (isInternalNavigationRef.current)
498
+ return;
480
499
  const newLocale = detectLocale();
481
500
  const previousLocale = locale;
482
501
  // Show navigation overlay immediately
483
502
  if (locale !== defaultLocale || newLocale !== defaultLocale) {
484
503
  setIsNavigationLoading(true);
485
504
  }
486
- // SET NAVIGATION FLAG & DISCONNECT OBSERVER
505
+ // SET NAVIGATION FLAG (MutationObserver callback will ignore while navigating)
487
506
  isNavigatingRef.current = true;
488
- if (observerRef.current) {
489
- observerRef.current.disconnect();
490
- }
491
507
  // Clear any pending navigation translation
492
508
  if (navigationTimeout)
493
509
  clearTimeout(navigationTimeout);
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect } from 'react';
2
- import { BrowserRouter, Routes, Route, Navigate, Outlet, useParams, useNavigate } from 'react-router-dom';
2
+ import { BrowserRouter, Routes, Route, Navigate, Outlet, useLocation, useParams, useNavigate } from 'react-router-dom';
3
3
  /**
4
4
  * NavigateExporter - Internal component that exports navigate function via ref
5
5
  */
@@ -17,13 +17,27 @@ function NavigateExporter({ navigateRef }) {
17
17
  */
18
18
  function LangGuard({ defaultLang, langs }) {
19
19
  const { lang } = useParams();
20
+ const location = useLocation();
20
21
  // If invalid language, redirect to default
21
22
  if (!lang || !langs.includes(lang)) {
22
- return React.createElement(Navigate, { to: `/${defaultLang}`, replace: true });
23
+ const parts = location.pathname.split('/').filter(Boolean);
24
+ // Drop the invalid locale segment (first)
25
+ if (parts.length > 0)
26
+ parts.shift();
27
+ const rest = parts.join('/');
28
+ const nextPath = `/${defaultLang}${rest ? `/${rest}` : ''}${location.search}${location.hash}`;
29
+ return React.createElement(Navigate, { to: nextPath, replace: true });
23
30
  }
24
31
  // Valid language - render children (user's routes)
25
32
  return React.createElement(Outlet, { context: { lang } });
26
33
  }
34
+ function RedirectToDefaultLang({ defaultLang }) {
35
+ const location = useLocation();
36
+ const nextPath = location.pathname === '/' || location.pathname === ''
37
+ ? `/${defaultLang}${location.search}${location.hash}`
38
+ : `/${defaultLang}${location.pathname}${location.search}${location.hash}`;
39
+ return React.createElement(Navigate, { to: nextPath, replace: true });
40
+ }
27
41
  /**
28
42
  * LangRouter - Drop-in replacement for BrowserRouter that automatically handles language routing
29
43
  *
@@ -56,5 +70,5 @@ export function LangRouter({ children, defaultLang, langs, navigateRef }) {
56
70
  React.createElement(Route, { path: ":lang/*", element: React.createElement(LangGuard, { defaultLang: defaultLang, langs: langs }) },
57
71
  React.createElement(Route, { index: true, element: React.createElement(React.Fragment, null, children) }),
58
72
  React.createElement(Route, { path: "*", element: React.createElement(React.Fragment, null, children) })),
59
- React.createElement(Route, { path: "*", element: React.createElement(Navigate, { to: `/${defaultLang}`, replace: true }) }))));
73
+ React.createElement(Route, { path: "*", element: React.createElement(RedirectToDefaultLang, { defaultLang: defaultLang }) }))));
60
74
  }
package/dist/index.d.ts CHANGED
@@ -22,4 +22,5 @@ export { useLangNavigate } from './hooks/useLangNavigate';
22
22
  export { useLovalingo } from './hooks/useLovalingo';
23
23
  export { useLovalingoTranslate } from './hooks/useLovalingoTranslate';
24
24
  export { useLovalingoEdit } from './hooks/useLovalingoEdit';
25
+ export { VERSION } from './version';
25
26
  export type { LovalingoConfig, LovalingoContextValue, Translation, Exclusion, HashTranslation } from './types';
package/dist/index.js CHANGED
@@ -25,3 +25,5 @@ export { useLangNavigate } from './hooks/useLangNavigate';
25
25
  export { useLovalingo } from './hooks/useLovalingo';
26
26
  export { useLovalingoTranslate } from './hooks/useLovalingoTranslate';
27
27
  export { useLovalingoEdit } from './hooks/useLovalingoEdit';
28
+ // Version
29
+ export { VERSION } from './version';
@@ -0,0 +1 @@
1
+ export declare const VERSION = "0.0.15";
@@ -0,0 +1 @@
1
+ export const VERSION = "0.0.15";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovalingo/lovalingo",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
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",