@sudobility/building_blocks 0.0.117 → 0.0.121

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.
@@ -3,95 +3,12 @@ import { Suspense, useRef, useEffect, useMemo, createContext, useContext } from
3
3
  import { useLocation, BrowserRouter } from "react-router-dom";
4
4
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
5
  import { HelmetProvider } from "react-helmet-async";
6
- import { initReactI18next, I18nextProvider } from "react-i18next";
6
+ import { I18nextProvider } from "react-i18next";
7
7
  import { NetworkProvider } from "@sudobility/devops-components";
8
8
  import { getFirebaseService, getNetworkService } from "@sudobility/di/web";
9
9
  import { InfoBanner } from "@sudobility/di_web";
10
10
  import { ThemeProvider, FontSize, Theme } from "@sudobility/components";
11
11
  import { useToast, ToastContainer, ToastProvider } from "@sudobility/components/ui/toast";
12
- import i18n from "i18next";
13
- import Backend from "i18next-http-backend";
14
- import LanguageDetector from "i18next-browser-languagedetector";
15
- const DEFAULT_SUPPORTED_LANGUAGES = ["en"];
16
- const DEFAULT_NAMESPACES = [
17
- "common",
18
- "home",
19
- "pricing",
20
- "docs",
21
- "dashboard",
22
- "auth",
23
- "privacy",
24
- "terms",
25
- "settings"
26
- ];
27
- function detectLanguageFromPath(supportedLanguages) {
28
- if (typeof window === "undefined") {
29
- return "en";
30
- }
31
- const pathLang = window.location.pathname.split("/")[1];
32
- if (pathLang && supportedLanguages.includes(pathLang)) {
33
- return pathLang;
34
- }
35
- try {
36
- const stored = localStorage.getItem("language");
37
- if (stored && supportedLanguages.includes(stored)) {
38
- return stored;
39
- }
40
- } catch {
41
- }
42
- return "en";
43
- }
44
- let initialized = false;
45
- function initializeI18n(config = {}) {
46
- if (initialized) {
47
- return i18n;
48
- }
49
- initialized = true;
50
- const {
51
- supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES,
52
- namespaces = DEFAULT_NAMESPACES,
53
- defaultNamespace = "common",
54
- loadPath = "/locales/{{lng}}/{{ns}}.json",
55
- debug = false
56
- } = config;
57
- i18n.use(Backend).use(LanguageDetector).use(initReactI18next).init({
58
- lng: detectLanguageFromPath(supportedLanguages),
59
- fallbackLng: {
60
- zh: ["zh", "en"],
61
- "zh-hant": ["zh-hant", "zh", "en"],
62
- default: ["en"]
63
- },
64
- initImmediate: false,
65
- supportedLngs: supportedLanguages,
66
- debug,
67
- nonExplicitSupportedLngs: true,
68
- interpolation: {
69
- escapeValue: false
70
- },
71
- backend: {
72
- loadPath
73
- },
74
- detection: {
75
- order: ["path", "localStorage", "navigator"],
76
- caches: ["localStorage"],
77
- lookupLocalStorage: "language",
78
- lookupFromPathIndex: 0
79
- },
80
- load: "languageOnly",
81
- preload: [],
82
- cleanCode: false,
83
- lowerCaseLng: false,
84
- defaultNS: defaultNamespace,
85
- ns: namespaces
86
- });
87
- return i18n;
88
- }
89
- function getI18n() {
90
- if (!initialized) {
91
- initializeI18n();
92
- }
93
- return i18n;
94
- }
95
12
  function DefaultLoadingFallback() {
96
13
  return /* @__PURE__ */ jsx("div", { className: "min-h-screen flex items-center justify-center bg-theme-bg-primary", children: /* @__PURE__ */ jsx("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" }) });
97
14
  }
@@ -199,7 +116,6 @@ function SudobilityApp({
199
116
  storageKeyPrefix = "sudobility",
200
117
  RouterWrapper
201
118
  }) {
202
- const i18nToUse = i18nInstance ?? getI18n();
203
119
  const NetworkProviderComponent = NetworkProviderProp === false ? null : NetworkProviderProp ?? DefaultNetworkProvider;
204
120
  const PageTrackerComponent = PageTrackerProp === false ? null : PageTrackerProp ?? DefaultPageTracker;
205
121
  const ThemeProviderComponent = ThemeProviderProp ?? createDefaultThemeProvider(storageKeyPrefix);
@@ -223,7 +139,7 @@ function SudobilityApp({
223
139
  routerContent = /* @__PURE__ */ jsx(NetworkProviderComponent, { children: routerContent });
224
140
  }
225
141
  routerContent = /* @__PURE__ */ jsx(ThemeProviderComponent, { children: routerContent });
226
- routerContent = /* @__PURE__ */ jsx(I18nextProvider, { i18n: i18nToUse, children: routerContent });
142
+ routerContent = /* @__PURE__ */ jsx(I18nextProvider, { i18n: i18nInstance, children: routerContent });
227
143
  return /* @__PURE__ */ jsx(HelmetProvider, { children: routerContent });
228
144
  }
229
145
  const STUB_SUBSCRIPTION_VALUE = {
@@ -248,8 +164,6 @@ export {
248
164
  SudobilityApp as S,
249
165
  SafeSubscriptionContext as a,
250
166
  STUB_SUBSCRIPTION_VALUE as b,
251
- getI18n as g,
252
- initializeI18n as i,
253
167
  useSafeSubscription as u
254
168
  };
255
- //# sourceMappingURL=SafeSubscriptionContext-BuMPCXVi.js.map
169
+ //# sourceMappingURL=SafeSubscriptionContext-CNuEKeqJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SafeSubscriptionContext-CNuEKeqJ.js","sources":["../src/components/app/SudobilityApp.tsx","../src/components/subscription/SafeSubscriptionContext.tsx"],"sourcesContent":["/**\n * SudobilityApp - Base app wrapper for all Sudobility apps\n *\n * Provides common infrastructure with sensible defaults:\n * - HelmetProvider for SEO\n * - I18nextProvider for localization\n * - ThemeProvider for theming (built-in default)\n * - NetworkProvider for online/offline status (built-in default)\n * - QueryClientProvider for TanStack Query (built-in default)\n * - ToastProvider for notifications (built-in default)\n * - BrowserRouter for routing\n * - InfoBanner for system notifications (built-in default)\n *\n * All providers have sensible defaults - only pass props to override.\n */\nimport {\n Suspense,\n ComponentType,\n ReactNode,\n useMemo,\n useRef,\n useEffect,\n} from 'react';\nimport { BrowserRouter } from 'react-router-dom';\nimport {\n QueryClient,\n QueryClientProvider,\n type QueryClient as QueryClientType,\n} from '@tanstack/react-query';\nimport { HelmetProvider } from 'react-helmet-async';\nimport { I18nextProvider } from 'react-i18next';\nimport type { i18n } from 'i18next';\nimport { useLocation } from 'react-router-dom';\nimport { NetworkProvider } from '@sudobility/devops-components';\nimport { getNetworkService, getFirebaseService } from '@sudobility/di/web';\nimport { InfoBanner } from '@sudobility/di_web';\nimport {\n ThemeProvider as SharedThemeProvider,\n Theme,\n FontSize,\n} from '@sudobility/components';\nimport {\n ToastProvider as SharedToastProvider,\n ToastContainer as SharedToastContainer,\n useToast,\n} from '@sudobility/components/ui/toast';\n\n/**\n * QueryClient type that's compatible across different package versions.\n * Uses structural typing to avoid cross-package type conflicts when using bun link.\n */\ntype QueryClientLike = {\n getDefaultOptions: () => unknown;\n getQueryCache: () => unknown;\n getMutationCache: () => unknown;\n};\n\n/**\n * i18n type that's compatible across different package versions.\n * Uses structural typing to avoid cross-package type conflicts when using bun link.\n */\ntype I18nLike = {\n language: string;\n languages: readonly string[];\n\n t: (...args: any[]) => any;\n};\n\nexport interface SudobilityAppProps {\n /** App routes and content */\n children: ReactNode;\n\n /**\n * i18next instance for localization.\n * Each app must pass its own configured i18n instance.\n */\n i18n: I18nLike;\n\n /**\n * TanStack Query client instance (optional).\n * Defaults to a QueryClient with sensible settings.\n */\n queryClient?: QueryClientLike;\n\n /**\n * Custom ThemeProvider component (optional).\n * Defaults to SharedThemeProvider from @sudobility/components.\n */\n ThemeProvider?: ComponentType<{ children: ReactNode }>;\n\n /**\n * Custom ToastProvider component (optional).\n * Defaults to ToastProvider from @sudobility/components.\n */\n ToastProvider?: ComponentType<{ children: ReactNode }>;\n\n /**\n * Toast container component rendered after routes (optional).\n * Defaults to a built-in container that renders toasts from context at bottom-right.\n */\n ToastContainer?: ComponentType;\n\n /**\n * Whether to show the InfoBanner for notifications/errors.\n * Defaults to true. Set to false to disable.\n */\n showInfoBanner?: boolean;\n\n /** Custom loading fallback for Suspense (optional) */\n LoadingFallback?: ComponentType;\n\n /**\n * Custom NetworkProvider component (optional).\n * By default, uses the built-in NetworkProvider with getNetworkService().\n * Set to false to disable network status tracking entirely.\n */\n NetworkProvider?: ComponentType<{ children: ReactNode }> | false;\n\n /**\n * Page tracker component for analytics (optional).\n * By default, uses Firebase Analytics to track page_view events.\n * Pass a custom component to override, or false to disable.\n */\n PageTracker?: ComponentType | false;\n\n /**\n * Additional providers to wrap around the router content.\n * These are rendered inside ToastProvider but outside BrowserRouter.\n * Use this for app-specific providers like ApiProvider, SettingsProvider, etc.\n */\n AppProviders?: ComponentType<{ children: ReactNode }>;\n\n /**\n * Storage key prefix for theme persistence (optional).\n * Defaults to \"sudobility\". Used for localStorage keys (e.g., \"sudobility-theme\").\n * Since localStorage is origin-scoped, apps on different domains don't need\n * different prefixes. Only override if running multiple app instances on the\n * same origin or if you want app-specific debugging visibility.\n */\n storageKeyPrefix?: string;\n\n /**\n * Custom router wrapper component (optional).\n * Defaults to BrowserRouter. Pass a fragment wrapper `({ children }) => <>{children}</>`\n * to skip the router entirely (useful when nesting inside an existing router).\n */\n RouterWrapper?: ComponentType<{ children: ReactNode }>;\n}\n\n/**\n * Default loading fallback component\n */\nfunction DefaultLoadingFallback() {\n return (\n <div className='min-h-screen flex items-center justify-center bg-theme-bg-primary'>\n <div className='animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600' />\n </div>\n );\n}\n\n/**\n * Default page tracker using Firebase Analytics.\n * Tracks page_view events on route changes.\n */\nfunction DefaultPageTracker(): null {\n const location = useLocation();\n const previousPathRef = useRef<string | null>(null);\n\n useEffect(() => {\n const currentPath = location.pathname;\n\n // Skip if same path\n if (previousPathRef.current === currentPath) {\n return;\n }\n\n previousPathRef.current = currentPath;\n\n // Track page view via Firebase Analytics\n try {\n const firebaseService = getFirebaseService();\n if (firebaseService?.analytics?.isSupported()) {\n firebaseService.analytics.logEvent('page_view', {\n page_path: currentPath,\n page_location: window.location.href,\n });\n }\n } catch {\n // Firebase not configured - silently skip tracking\n }\n }, [location.pathname]);\n\n return null;\n}\n\n/**\n * Default network provider that uses getNetworkService()\n */\nfunction DefaultNetworkProvider({ children }: { children: ReactNode }) {\n const networkService = useMemo(() => getNetworkService(), []);\n return (\n <NetworkProvider networkService={networkService}>\n {children}\n </NetworkProvider>\n );\n}\n\n/**\n * Create default QueryClient with sensible settings\n */\nfunction createDefaultQueryClient(): QueryClient {\n return new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 5 * 60 * 1000, // 5 minutes\n gcTime: 10 * 60 * 1000, // 10 minutes\n // Smart retry: don't retry on 4xx client errors, retry up to 3 times otherwise\n retry: (failureCount, error: unknown) => {\n const statusCode = (error as { statusCode?: number })?.statusCode;\n if (statusCode && statusCode >= 400 && statusCode < 500) {\n return false;\n }\n return failureCount < 3;\n },\n // Exponential backoff: 1s, 2s, 4s... up to 30s\n retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),\n refetchOnWindowFocus: true,\n },\n mutations: {\n retry: false,\n },\n },\n });\n}\n\n// Singleton default QueryClient\nlet defaultQueryClient: QueryClient | null = null;\nfunction getDefaultQueryClient(): QueryClient {\n if (!defaultQueryClient) {\n defaultQueryClient = createDefaultQueryClient();\n }\n return defaultQueryClient;\n}\n\n/**\n * Default theme provider using SharedThemeProvider from @sudobility/components\n */\nfunction createDefaultThemeProvider(storageKeyPrefix: string) {\n return function DefaultThemeProvider({ children }: { children: ReactNode }) {\n return (\n <SharedThemeProvider\n themeStorageKey={`${storageKeyPrefix}-theme`}\n fontSizeStorageKey={`${storageKeyPrefix}-font-size`}\n defaultTheme={Theme.LIGHT}\n defaultFontSize={FontSize.MEDIUM}\n >\n {children}\n </SharedThemeProvider>\n );\n };\n}\n\n/**\n * Default toast provider using SharedToastProvider from @sudobility/components\n */\nfunction DefaultToastProvider({ children }: { children: ReactNode }) {\n return <SharedToastProvider>{children}</SharedToastProvider>;\n}\n\n/**\n * Default toast container that renders toasts from context.\n * Wraps SharedToastContainer with useToast consumer.\n */\nfunction DefaultToastContainer() {\n const { toasts, removeToast } = useToast();\n\n if (toasts.length === 0) return null;\n\n return (\n <SharedToastContainer\n toasts={toasts}\n onDismiss={removeToast}\n position='bottom-right'\n />\n );\n}\n\n/**\n * SudobilityApp - Base app wrapper for all Sudobility apps\n *\n * @example\n * ```tsx\n * // Minimal usage - only i18n is required\n * import { SudobilityApp } from '@sudobility/building_blocks';\n * import i18n from './i18n';\n *\n * function App() {\n * return (\n * <SudobilityApp i18n={i18n}>\n * <Routes>\n * <Route path=\"/\" element={<HomePage />} />\n * </Routes>\n * </SudobilityApp>\n * );\n * }\n *\n * // With custom providers\n * function App() {\n * return (\n * <SudobilityApp\n * i18n={i18n}\n * ThemeProvider={MyCustomThemeProvider}\n * AppProviders={ApiProvider}\n * storageKeyPrefix=\"myapp\"\n * >\n * <Routes>\n * <Route path=\"/\" element={<HomePage />} />\n * </Routes>\n * </SudobilityApp>\n * );\n * }\n * ```\n */\nexport function SudobilityApp({\n children,\n i18n: i18nInstance,\n queryClient,\n ThemeProvider: ThemeProviderProp,\n ToastProvider: ToastProviderProp,\n ToastContainer,\n showInfoBanner = true,\n LoadingFallback = DefaultLoadingFallback,\n NetworkProvider: NetworkProviderProp,\n PageTracker: PageTrackerProp,\n AppProviders,\n storageKeyPrefix = 'sudobility',\n RouterWrapper,\n}: SudobilityAppProps) {\n // Determine which providers to use (custom or default)\n const NetworkProviderComponent =\n NetworkProviderProp === false\n ? null\n : (NetworkProviderProp ?? DefaultNetworkProvider);\n\n const PageTrackerComponent =\n PageTrackerProp === false ? null : (PageTrackerProp ?? DefaultPageTracker);\n\n const ThemeProviderComponent =\n ThemeProviderProp ?? createDefaultThemeProvider(storageKeyPrefix);\n\n const ToastProviderComponent = ToastProviderProp ?? DefaultToastProvider;\n\n const ToastContainerComponent = ToastContainer ?? DefaultToastContainer;\n\n const queryClientInstance = queryClient ?? getDefaultQueryClient();\n\n // Use custom RouterWrapper or default BrowserRouter\n const RouterWrapperComponent = RouterWrapper ?? BrowserRouter;\n\n // Build the router content\n let routerContent: ReactNode = (\n <>\n {PageTrackerComponent && <PageTrackerComponent />}\n <Suspense fallback={<LoadingFallback />}>{children}</Suspense>\n <ToastContainerComponent />\n {showInfoBanner && <InfoBanner />}\n </>\n );\n\n // Wrap with Router\n routerContent = (\n <RouterWrapperComponent>{routerContent}</RouterWrapperComponent>\n );\n\n // Wrap with AppProviders if provided\n if (AppProviders) {\n routerContent = <AppProviders>{routerContent}</AppProviders>;\n }\n\n // Wrap with ToastProvider\n routerContent = (\n <ToastProviderComponent>{routerContent}</ToastProviderComponent>\n );\n\n // Wrap with QueryClientProvider\n routerContent = (\n <QueryClientProvider client={queryClientInstance as QueryClientType}>\n {routerContent}\n </QueryClientProvider>\n );\n\n // Wrap with NetworkProvider (uses default if not explicitly disabled)\n if (NetworkProviderComponent) {\n routerContent = (\n <NetworkProviderComponent>{routerContent}</NetworkProviderComponent>\n );\n }\n\n // Wrap with ThemeProvider\n routerContent = (\n <ThemeProviderComponent>{routerContent}</ThemeProviderComponent>\n );\n\n // Wrap with I18nextProvider\n routerContent = (\n <I18nextProvider i18n={i18nInstance as i18n}>\n {routerContent}\n </I18nextProvider>\n );\n\n // Wrap with HelmetProvider\n return <HelmetProvider>{routerContent}</HelmetProvider>;\n}\n\nexport default SudobilityApp;\n","/**\n * Safe Subscription Context\n *\n * Provides a context that can be safely used even when subscription\n * provider isn't loaded yet. Returns stub values for unauthenticated users.\n */\n\nimport { createContext, useContext } from 'react';\nimport type { SubscriptionContextValue } from '@sudobility/subscription-components';\n\n/**\n * Stub subscription value for unauthenticated users or when\n * subscription provider hasn't loaded yet.\n */\nexport const STUB_SUBSCRIPTION_VALUE: SubscriptionContextValue = {\n products: [],\n currentSubscription: null,\n isLoading: false,\n error: null,\n initialize: () => Promise.resolve(),\n purchase: () => Promise.resolve(false),\n restore: () => Promise.resolve(false),\n refresh: () => Promise.resolve(),\n clearError: () => {},\n};\n\n/**\n * Context that provides subscription state with safe defaults.\n */\nexport const SafeSubscriptionContext = createContext<SubscriptionContextValue>(\n STUB_SUBSCRIPTION_VALUE\n);\n\n/**\n * Hook to safely access subscription context.\n * Returns stub values if provider isn't available.\n */\nexport function useSafeSubscription(): SubscriptionContextValue {\n return useContext(SafeSubscriptionContext);\n}\n"],"names":["SharedThemeProvider","SharedToastProvider","SharedToastContainer","ToastContainer"],"mappings":"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"}
@@ -36,11 +36,10 @@ export interface SudobilityAppProps {
36
36
  /** App routes and content */
37
37
  children: ReactNode;
38
38
  /**
39
- * i18next instance for localization (optional).
40
- * Defaults to built-in i18n that loads translations from /locales/.
41
- * Pass your own if you need custom configuration.
39
+ * i18next instance for localization.
40
+ * Each app must pass its own configured i18n instance.
42
41
  */
43
- i18n?: I18nLike;
42
+ i18n: I18nLike;
44
43
  /**
45
44
  * TanStack Query client instance (optional).
46
45
  * Defaults to a QueryClient with sensible settings.
@@ -28,6 +28,8 @@ export interface AppPageLayoutProps extends VariantProps<typeof layoutVariants>
28
28
  contentClassName?: string;
29
29
  /** Custom className for the main element */
30
30
  mainClassName?: string;
31
+ /** Optional aspect ratio (width / height) for content area. When set, children are placed inside a container with fixed aspect ratio using AspectFit behavior. */
32
+ aspectRatio?: number;
31
33
  }
32
34
  /**
33
35
  * AppPageLayout - Layout wrapper combining TopBar, Breadcrumbs, Content, and Footer.
package/dist/firebase.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx, Fragment } from "react/jsx-runtime";
2
- import { S as SudobilityApp, a as SafeSubscriptionContext, b as STUB_SUBSCRIPTION_VALUE } from "./SafeSubscriptionContext-BuMPCXVi.js";
2
+ import { S as SudobilityApp, a as SafeSubscriptionContext, b as STUB_SUBSCRIPTION_VALUE } from "./SafeSubscriptionContext-CNuEKeqJ.js";
3
3
  import { useAuthStatus, AuthProvider } from "@sudobility/auth-components";
4
4
  import { getFirebaseAuth, FirebaseAuthNetworkService, initializeFirebaseAuth, getFirebaseErrorMessage } from "@sudobility/auth_lib";
5
5
  import { createContext, useState, useEffect, useCallback, useRef, useMemo, useContext, lazy, Suspense } from "react";
package/dist/index.js CHANGED
@@ -7,11 +7,15 @@ import { ChevronDownIcon, CalendarDaysIcon, PaintBrushIcon, LanguageIcon, Chevro
7
7
  import { GRADIENT_CLASSES, textVariants } from "@sudobility/design";
8
8
  import { cva } from "class-variance-authority";
9
9
  import { createUserWithEmailAndPassword, signInWithEmailAndPassword, GoogleAuthProvider, signInWithPopup } from "firebase/auth";
10
- import { b, a, S, g, i, u } from "./SafeSubscriptionContext-BuMPCXVi.js";
10
+ import { b, a, S, u } from "./SafeSubscriptionContext-CNuEKeqJ.js";
11
11
  import { SubscriptionLayout, SubscriptionTile, SegmentedControl } from "@sudobility/subscription-components";
12
12
  import { useSubscriptionPeriods, useSubscriptionForPeriod, useSubscribable, useUserSubscription, refreshSubscription } from "@sudobility/subscription_lib";
13
- import { ToastContainer, ToastProvider, useToast } from "@sudobility/components/ui/toast";
13
+ import i18n from "i18next";
14
14
  import { default as default2 } from "i18next";
15
+ import { initReactI18next } from "react-i18next";
16
+ import Backend from "i18next-http-backend";
17
+ import LanguageDetector from "i18next-browser-languagedetector";
18
+ import { ToastContainer, ToastProvider, useToast } from "@sudobility/components/ui/toast";
15
19
  function cn(...inputs) {
16
20
  return twMerge(clsx(inputs));
17
21
  }
@@ -989,8 +993,17 @@ const AppPageLayout = ({
989
993
  layoutMode = "standard",
990
994
  className,
991
995
  contentClassName,
992
- mainClassName
996
+ mainClassName,
997
+ aspectRatio
993
998
  }) => {
999
+ const content = aspectRatio ? /* @__PURE__ */ jsx(
1000
+ "div",
1001
+ {
1002
+ className: "mx-auto max-h-full max-w-full overflow-auto",
1003
+ style: { aspectRatio },
1004
+ children
1005
+ }
1006
+ ) : children;
994
1007
  return /* @__PURE__ */ jsx(LayoutProvider, { mode: layoutMode, children: /* @__PURE__ */ jsxs("div", { className: cn(layoutVariants({ background }), className), children: [
995
1008
  /* @__PURE__ */ jsx("header", { children: topBar }),
996
1009
  breadcrumbs && breadcrumbs.items && breadcrumbs.items.length > 0 && /* @__PURE__ */ jsx(AppBreadcrumbs, { ...breadcrumbs }),
@@ -1003,7 +1016,7 @@ const AppPageLayout = ({
1003
1016
  paddingClasses[contentPadding],
1004
1017
  contentClassName
1005
1018
  ),
1006
- children
1019
+ children: content
1007
1020
  }
1008
1021
  ) }),
1009
1022
  footer && /* @__PURE__ */ jsx("footer", { children: footer })
@@ -2508,6 +2521,86 @@ function AppPricingPage({
2508
2521
  ] })
2509
2522
  ] });
2510
2523
  }
2524
+ const DEFAULT_SUPPORTED_LANGUAGES = ["en"];
2525
+ const DEFAULT_NAMESPACES = [
2526
+ "common",
2527
+ "home",
2528
+ "pricing",
2529
+ "docs",
2530
+ "dashboard",
2531
+ "auth",
2532
+ "privacy",
2533
+ "terms",
2534
+ "settings"
2535
+ ];
2536
+ function detectLanguageFromPath(supportedLanguages) {
2537
+ if (typeof window === "undefined") {
2538
+ return "en";
2539
+ }
2540
+ const pathLang = window.location.pathname.split("/")[1];
2541
+ if (pathLang && supportedLanguages.includes(pathLang)) {
2542
+ return pathLang;
2543
+ }
2544
+ try {
2545
+ const stored = localStorage.getItem("language");
2546
+ if (stored && supportedLanguages.includes(stored)) {
2547
+ return stored;
2548
+ }
2549
+ } catch {
2550
+ }
2551
+ return "en";
2552
+ }
2553
+ let initialized = false;
2554
+ function initializeI18n(config = {}) {
2555
+ if (initialized) {
2556
+ return i18n;
2557
+ }
2558
+ initialized = true;
2559
+ const {
2560
+ supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES,
2561
+ namespaces = DEFAULT_NAMESPACES,
2562
+ defaultNamespace = "common",
2563
+ loadPath = "/locales/{{lng}}/{{ns}}.json",
2564
+ debug = false
2565
+ } = config;
2566
+ i18n.use(Backend).use(LanguageDetector).use(initReactI18next).init({
2567
+ lng: detectLanguageFromPath(supportedLanguages),
2568
+ fallbackLng: {
2569
+ zh: ["zh", "en"],
2570
+ "zh-hant": ["zh-hant", "zh", "en"],
2571
+ default: ["en"]
2572
+ },
2573
+ initImmediate: false,
2574
+ supportedLngs: supportedLanguages,
2575
+ debug,
2576
+ nonExplicitSupportedLngs: true,
2577
+ interpolation: {
2578
+ escapeValue: false
2579
+ },
2580
+ backend: {
2581
+ loadPath
2582
+ },
2583
+ detection: {
2584
+ order: ["path", "localStorage", "navigator"],
2585
+ caches: ["localStorage"],
2586
+ lookupLocalStorage: "language",
2587
+ lookupFromPathIndex: 0
2588
+ },
2589
+ load: "languageOnly",
2590
+ preload: [],
2591
+ cleanCode: false,
2592
+ lowerCaseLng: false,
2593
+ defaultNS: defaultNamespace,
2594
+ ns: namespaces
2595
+ });
2596
+ return i18n;
2597
+ }
2598
+ function getI18n() {
2599
+ if (!initialized) {
2600
+ initializeI18n();
2601
+ }
2602
+ return i18n;
2603
+ }
2511
2604
  export {
2512
2605
  AppBreadcrumbs,
2513
2606
  AppFooter,
@@ -2536,9 +2629,9 @@ export {
2536
2629
  ToastContainer,
2537
2630
  ToastProvider,
2538
2631
  cn,
2539
- g as getI18n,
2632
+ getI18n,
2540
2633
  default2 as i18n,
2541
- i as initializeI18n,
2634
+ initializeI18n,
2542
2635
  isRTL,
2543
2636
  u as useSafeSubscription,
2544
2637
  useToast