@djangocfg/layouts 2.1.111 → 2.1.113

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 CHANGED
@@ -168,6 +168,75 @@ import { PublicLayout, PrivateLayout, AuthLayout } from '@djangocfg/layouts';
168
168
  | `AdminLayout` | Admin panel layout |
169
169
  | `ProfileLayout` | User profile pages |
170
170
 
171
+ ### i18n Support
172
+
173
+ Pass `i18n` config to AppLayout for locale switching in all layouts:
174
+
175
+ ```tsx
176
+ import { AppLayout } from '@djangocfg/layouts';
177
+ import { useLocaleSwitcher } from '@djangocfg/nextjs/i18n/client';
178
+
179
+ function RootLayout({ children }) {
180
+ const { locale, locales, changeLocale } = useLocaleSwitcher();
181
+
182
+ return (
183
+ <AppLayout
184
+ // ... other configs
185
+ i18n={{
186
+ locale,
187
+ locales,
188
+ onLocaleChange: changeLocale,
189
+ }}
190
+ >
191
+ {children}
192
+ </AppLayout>
193
+ );
194
+ }
195
+ ```
196
+
197
+ The `LocaleSwitcher` component will automatically appear in PublicLayout and PrivateLayout headers.
198
+
199
+ ### LocaleSwitcher Component
200
+
201
+ A presentational locale switcher component that can be used standalone:
202
+
203
+ ```tsx
204
+ import { LocaleSwitcher } from '@djangocfg/layouts';
205
+
206
+ // Basic usage (pass locale data via props)
207
+ <LocaleSwitcher
208
+ locale="en"
209
+ locales={['en', 'ru', 'ko']}
210
+ onChange={(locale) => router.push(`/${locale}`)}
211
+ />
212
+
213
+ // With custom labels and styling
214
+ <LocaleSwitcher
215
+ locale={currentLocale}
216
+ locales={['en', 'ru', 'ko']}
217
+ onChange={handleLocaleChange}
218
+ labels={{ en: 'English', ru: 'Русский', ko: '한국어' }}
219
+ variant="outline"
220
+ size="sm"
221
+ showIcon={true}
222
+ />
223
+ ```
224
+
225
+ **Props:**
226
+ | Prop | Type | Default | Description |
227
+ |------|------|---------|-------------|
228
+ | `locale` | `string` | - | Current locale code |
229
+ | `locales` | `string[]` | - | Available locale codes |
230
+ | `onChange` | `(locale: string) => void` | - | Callback when locale changes |
231
+ | `labels` | `Record<string, string>` | Built-in | Custom labels for locales |
232
+ | `showCode` | `boolean` | `false` | Show locale code with label |
233
+ | `variant` | `'ghost' \| 'outline' \| 'default'` | `'ghost'` | Button variant |
234
+ | `size` | `'sm' \| 'default' \| 'lg' \| 'icon'` | `'sm'` | Button size |
235
+ | `showIcon` | `boolean` | `true` | Show globe icon |
236
+ | `className` | `string` | - | Custom CSS class |
237
+
238
+ > **Smart version:** For automatic locale detection with next-intl hooks, use `@djangocfg/nextjs/i18n/components` which wraps this component.
239
+
171
240
  > **Extension Layouts:** Additional layouts like `SupportLayout` and `PaymentsLayout` are available in extension packages:
172
241
  > - `@djangocfg/ext-support` - Support ticket layouts
173
242
  > - `@djangocfg/ext-payments` - Payment flow layouts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.111",
3
+ "version": "2.1.113",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -74,12 +74,12 @@
74
74
  "check": "tsc --noEmit"
75
75
  },
76
76
  "peerDependencies": {
77
- "@djangocfg/api": "^2.1.111",
78
- "@djangocfg/centrifugo": "^2.1.111",
79
- "@djangocfg/i18n": "^2.1.111",
80
- "@djangocfg/ui-core": "^2.1.111",
81
- "@djangocfg/ui-nextjs": "^2.1.111",
82
- "@djangocfg/ui-tools": "^2.1.111",
77
+ "@djangocfg/api": "^2.1.113",
78
+ "@djangocfg/centrifugo": "^2.1.113",
79
+ "@djangocfg/i18n": "^2.1.113",
80
+ "@djangocfg/ui-core": "^2.1.113",
81
+ "@djangocfg/ui-nextjs": "^2.1.113",
82
+ "@djangocfg/ui-tools": "^2.1.113",
83
83
  "@hookform/resolvers": "^5.2.2",
84
84
  "consola": "^3.4.2",
85
85
  "lucide-react": "^0.545.0",
@@ -102,13 +102,13 @@
102
102
  "uuid": "^11.1.0"
103
103
  },
104
104
  "devDependencies": {
105
- "@djangocfg/api": "^2.1.111",
106
- "@djangocfg/i18n": "^2.1.111",
107
- "@djangocfg/centrifugo": "^2.1.111",
108
- "@djangocfg/typescript-config": "^2.1.111",
109
- "@djangocfg/ui-core": "^2.1.111",
110
- "@djangocfg/ui-nextjs": "^2.1.111",
111
- "@djangocfg/ui-tools": "^2.1.111",
105
+ "@djangocfg/api": "^2.1.113",
106
+ "@djangocfg/i18n": "^2.1.113",
107
+ "@djangocfg/centrifugo": "^2.1.113",
108
+ "@djangocfg/typescript-config": "^2.1.113",
109
+ "@djangocfg/ui-core": "^2.1.113",
110
+ "@djangocfg/ui-nextjs": "^2.1.113",
111
+ "@djangocfg/ui-tools": "^2.1.113",
112
112
  "@types/node": "^24.7.2",
113
113
  "@types/react": "^19.1.0",
114
114
  "@types/react-dom": "^19.1.0",
@@ -85,26 +85,33 @@ function determineLayoutMode(
85
85
  return 'public';
86
86
  }
87
87
 
88
+ /** i18n configuration for locale switching */
89
+ export interface I18nLayoutConfig {
90
+ /** Current locale */
91
+ locale: string;
92
+ /** Available locales */
93
+ locales: string[];
94
+ /** Callback when locale changes */
95
+ onLocaleChange: (locale: string) => void;
96
+ }
97
+
98
+ /** Layout configuration with component and enabled paths */
99
+ interface LayoutConfig {
100
+ component: React.ComponentType<{ children: ReactNode; i18n?: I18nLayoutConfig }>;
101
+ enabledPath?: string | string[];
102
+ }
103
+
88
104
  export interface AppLayoutProps {
89
105
  children: ReactNode;
90
106
 
91
107
  /** Public layout component with enabled paths */
92
- publicLayout?: {
93
- component: React.ComponentType<{ children: ReactNode }>;
94
- enabledPath?: string | string[];
95
- };
108
+ publicLayout?: LayoutConfig;
96
109
 
97
110
  /** Private layout component with enabled paths */
98
- privateLayout?: {
99
- component: React.ComponentType<{ children: ReactNode }>;
100
- enabledPath?: string | string[];
101
- };
111
+ privateLayout?: LayoutConfig;
102
112
 
103
113
  /** Admin layout component with enabled paths */
104
- adminLayout?: {
105
- component: React.ComponentType<{ children: ReactNode }>;
106
- enabledPath?: string | string[];
107
- };
114
+ adminLayout?: LayoutConfig;
108
115
 
109
116
  /**
110
117
  * Paths that render without any layout wrapper (fullscreen pages)
@@ -142,6 +149,9 @@ export interface AppLayoutProps {
142
149
 
143
150
  /** Push Notifications configuration */
144
151
  pushNotifications?: PushNotificationsConfig;
152
+
153
+ /** i18n configuration for locale switching (applies to all layouts) */
154
+ i18n?: I18nLayoutConfig;
145
155
  }
146
156
 
147
157
  /**
@@ -156,6 +166,7 @@ function AppLayoutContent({
156
166
  privateLayout,
157
167
  adminLayout,
158
168
  noLayoutPaths,
169
+ i18n,
159
170
  }: AppLayoutProps) {
160
171
  const pathname = usePathname();
161
172
 
@@ -188,7 +199,9 @@ function AppLayoutContent({
188
199
  return (
189
200
  <ClientOnly>
190
201
  <Suspense>
191
- <privateLayout.component>{children}</privateLayout.component>
202
+ <privateLayout.component i18n={i18n}>
203
+ {children}
204
+ </privateLayout.component>
192
205
  </Suspense>
193
206
  </ClientOnly>
194
207
  );
@@ -199,7 +212,9 @@ function AppLayoutContent({
199
212
  return (
200
213
  <ClientOnly>
201
214
  <Suspense>
202
- <adminLayout.component>{children}</adminLayout.component>
215
+ <adminLayout.component i18n={i18n}>
216
+ {children}
217
+ </adminLayout.component>
203
218
  </Suspense>
204
219
  </ClientOnly>
205
220
  );
@@ -207,14 +222,20 @@ function AppLayoutContent({
207
222
  case 'private':
208
223
  if (!privateLayout) {
209
224
  if (publicLayout) {
210
- return <publicLayout.component>{children}</publicLayout.component>;
225
+ return (
226
+ <publicLayout.component i18n={i18n}>
227
+ {children}
228
+ </publicLayout.component>
229
+ );
211
230
  }
212
231
  return children;
213
232
  }
214
233
  return (
215
234
  <ClientOnly>
216
235
  <Suspense>
217
- <privateLayout.component>{children}</privateLayout.component>
236
+ <privateLayout.component i18n={i18n}>
237
+ {children}
238
+ </privateLayout.component>
218
239
  </Suspense>
219
240
  </ClientOnly>
220
241
  );
@@ -225,7 +246,11 @@ function AppLayoutContent({
225
246
  if (!publicLayout) {
226
247
  return children;
227
248
  }
228
- return <publicLayout.component>{children}</publicLayout.component>;
249
+ return (
250
+ <publicLayout.component i18n={i18n}>
251
+ {children}
252
+ </publicLayout.component>
253
+ );
229
254
  }
230
255
  };
231
256
 
@@ -91,6 +91,8 @@ export interface HeaderConfig {
91
91
  authPath?: string;
92
92
  }
93
93
 
94
+ import type { I18nLayoutConfig } from '../AppLayout/AppLayout';
95
+
94
96
  export interface PrivateLayoutProps {
95
97
  children: ReactNode;
96
98
  /** Sidebar configuration */
@@ -99,6 +101,8 @@ export interface PrivateLayoutProps {
99
101
  header?: HeaderConfig;
100
102
  /** Content padding */
101
103
  contentPadding?: 'none' | 'default';
104
+ /** i18n configuration for locale switching */
105
+ i18n?: I18nLayoutConfig;
102
106
  }
103
107
 
104
108
  export function PrivateLayout({
@@ -106,6 +110,7 @@ export function PrivateLayout({
106
110
  sidebar,
107
111
  header,
108
112
  contentPadding = 'default',
113
+ i18n,
109
114
  }: PrivateLayoutProps) {
110
115
  const { isAuthenticated, isLoading, saveRedirectUrl } = useAuth();
111
116
  const router = useRouter();
@@ -145,7 +150,9 @@ export function PrivateLayout({
145
150
  {/* Main content area */}
146
151
  <SidebarInset className="flex flex-col">
147
152
  {/* Header with sidebar trigger */}
148
- {(header || isAuthenticated) && <PrivateHeader header={header} />}
153
+ {(header || isAuthenticated) && (
154
+ <PrivateHeader header={header} i18n={i18n} />
155
+ )}
149
156
 
150
157
  {/* Page content */}
151
158
  <PrivateContent padding={contentPadding}>{children}</PrivateContent>
@@ -13,15 +13,19 @@ import { Separator } from '@djangocfg/ui-nextjs/components';
13
13
  import { SidebarTrigger } from '@djangocfg/ui-nextjs/components';
14
14
  import { ThemeToggle } from '@djangocfg/ui-nextjs/theme';
15
15
 
16
+ import { LocaleSwitcher } from '../../_components/LocaleSwitcher';
16
17
  import { UserMenu } from '../../_components/UserMenu';
17
18
 
18
19
  import type { HeaderConfig } from '../PrivateLayout';
20
+ import type { I18nLayoutConfig } from '../../AppLayout/AppLayout';
19
21
 
20
22
  interface PrivateHeaderProps {
21
23
  header?: HeaderConfig;
24
+ /** i18n configuration for locale switching */
25
+ i18n?: I18nLayoutConfig;
22
26
  }
23
27
 
24
- export function PrivateHeader({ header }: PrivateHeaderProps) {
28
+ export function PrivateHeader({ header, i18n }: PrivateHeaderProps) {
25
29
  const { user, logout } = useAuth();
26
30
 
27
31
  return (
@@ -43,6 +47,15 @@ export function PrivateHeader({ header }: PrivateHeaderProps) {
43
47
 
44
48
  {/* Right side */}
45
49
  <div className="flex items-center gap-3">
50
+ {/* Locale Switcher */}
51
+ {i18n && (
52
+ <LocaleSwitcher
53
+ locale={i18n.locale}
54
+ locales={i18n.locales}
55
+ onChange={i18n.onLocaleChange}
56
+ />
57
+ )}
58
+
46
59
  {/* Theme Toggle */}
47
60
  <ThemeToggle />
48
61
 
@@ -34,6 +34,8 @@ import { ReactNode, useEffect, useState } from 'react';
34
34
  import { PublicMobileDrawer, PublicNavigation } from './components';
35
35
 
36
36
  import type { NavigationItem, UserMenuConfig } from '../types';
37
+ import type { I18nLayoutConfig } from '../AppLayout/AppLayout';
38
+
37
39
  export interface PublicLayoutProps {
38
40
  children: ReactNode;
39
41
  /** Logo path or URL */
@@ -44,6 +46,8 @@ export interface PublicLayoutProps {
44
46
  navigation?: NavigationItem[];
45
47
  /** User menu configuration (optional, uses useAuth() for authentication state) */
46
48
  userMenu?: UserMenuConfig;
49
+ /** i18n configuration for locale switching */
50
+ i18n?: I18nLayoutConfig;
47
51
  }
48
52
 
49
53
  export function PublicLayout({
@@ -52,6 +56,7 @@ export function PublicLayout({
52
56
  siteName = 'App',
53
57
  navigation = [],
54
58
  userMenu,
59
+ i18n,
55
60
  }: PublicLayoutProps) {
56
61
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
57
62
  const pathname = usePathname();
@@ -69,6 +74,7 @@ export function PublicLayout({
69
74
  siteName={siteName}
70
75
  navigation={navigation}
71
76
  userMenu={userMenu}
77
+ i18n={i18n}
72
78
  onMobileMenuClick={() => setMobileMenuOpen(true)}
73
79
  />
74
80
 
@@ -17,9 +17,11 @@ import { useIsMobile } from '@djangocfg/ui-core/hooks';
17
17
  import { cn } from '@djangocfg/ui-core/lib';
18
18
  import { ThemeToggle } from '@djangocfg/ui-nextjs/theme';
19
19
 
20
+ import { LocaleSwitcher } from '../../_components/LocaleSwitcher';
20
21
  import { UserMenu } from '../../_components/UserMenu';
21
22
 
22
23
  import type { NavigationItem, UserMenuConfig } from '../../types';
24
+ import type { I18nLayoutConfig } from '../../AppLayout/AppLayout';
23
25
 
24
26
  interface PublicNavigationProps {
25
27
  logo?: string;
@@ -27,6 +29,8 @@ interface PublicNavigationProps {
27
29
  navigation: NavigationItem[];
28
30
  userMenu?: UserMenuConfig;
29
31
  onMobileMenuClick: () => void;
32
+ /** i18n configuration for locale switching */
33
+ i18n?: I18nLayoutConfig;
30
34
  }
31
35
 
32
36
  export function PublicNavigation({
@@ -35,6 +39,7 @@ export function PublicNavigation({
35
39
  navigation,
36
40
  userMenu,
37
41
  onMobileMenuClick,
42
+ i18n,
38
43
  }: PublicNavigationProps) {
39
44
  const { isAuthenticated } = useAuth();
40
45
  const isMobile = useIsMobile();
@@ -75,6 +80,15 @@ export function PublicNavigation({
75
80
  <div className="flex items-center gap-4">
76
81
  {!isMobile && (
77
82
  <>
83
+ {/* Locale Switcher */}
84
+ {i18n && (
85
+ <LocaleSwitcher
86
+ locale={i18n.locale}
87
+ locales={i18n.locales}
88
+ onChange={i18n.onLocaleChange}
89
+ />
90
+ )}
91
+
78
92
  {/* Theme Toggle */}
79
93
  <ThemeToggle />
80
94
 
@@ -0,0 +1,114 @@
1
+ /**
2
+ * LocaleSwitcher Component (Presentational)
3
+ *
4
+ * "Dumb" locale switcher that receives data via props.
5
+ * For the "smart" version with next-intl hooks, use @djangocfg/nextjs/i18n/components
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * <LocaleSwitcher
10
+ * locale="en"
11
+ * locales={['en', 'ru', 'ko']}
12
+ * onChange={(locale) => router.push(`/${locale}`)}
13
+ * />
14
+ * ```
15
+ */
16
+
17
+ 'use client';
18
+
19
+ import { Globe } from 'lucide-react';
20
+
21
+ import {
22
+ Button,
23
+ DropdownMenu,
24
+ DropdownMenuContent,
25
+ DropdownMenuItem,
26
+ DropdownMenuTrigger,
27
+ } from '@djangocfg/ui-core/components';
28
+
29
+ // Default locale labels
30
+ const DEFAULT_LABELS: Record<string, string> = {
31
+ en: 'English',
32
+ ru: 'Русский',
33
+ ko: '한국어',
34
+ zh: '中文',
35
+ ja: '日本語',
36
+ es: 'Español',
37
+ fr: 'Français',
38
+ de: 'Deutsch',
39
+ pt: 'Português',
40
+ it: 'Italiano',
41
+ ar: 'العربية',
42
+ hi: 'हिन्दी',
43
+ tr: 'Türkçe',
44
+ pl: 'Polski',
45
+ nl: 'Nederlands',
46
+ uk: 'Українська',
47
+ };
48
+
49
+ export interface LocaleSwitcherProps {
50
+ /** Current locale */
51
+ locale: string;
52
+ /** Available locales */
53
+ locales: string[];
54
+ /** Callback when locale changes */
55
+ onChange: (locale: string) => void;
56
+ /** Custom labels for locales */
57
+ labels?: Record<string, string>;
58
+ /** Show locale code instead of/with label */
59
+ showCode?: boolean;
60
+ /** Button variant */
61
+ variant?: 'ghost' | 'outline' | 'default';
62
+ /** Button size */
63
+ size?: 'sm' | 'default' | 'lg' | 'icon';
64
+ /** Show icon */
65
+ showIcon?: boolean;
66
+ /** Custom className */
67
+ className?: string;
68
+ }
69
+
70
+ export function LocaleSwitcher({
71
+ locale,
72
+ locales,
73
+ onChange,
74
+ labels = {},
75
+ showCode = false,
76
+ variant = 'ghost',
77
+ size = 'sm',
78
+ showIcon = true,
79
+ className,
80
+ }: LocaleSwitcherProps) {
81
+ const allLabels = { ...DEFAULT_LABELS, ...labels };
82
+
83
+ const getLabel = (code: string) => {
84
+ const label = allLabels[code] || code.toUpperCase();
85
+ if (showCode) {
86
+ return `${code.toUpperCase()} - ${label}`;
87
+ }
88
+ return label;
89
+ };
90
+
91
+ const currentLabel = showCode ? locale.toUpperCase() : getLabel(locale);
92
+
93
+ return (
94
+ <DropdownMenu>
95
+ <DropdownMenuTrigger asChild>
96
+ <Button variant={variant} size={size} className={className}>
97
+ {showIcon && <Globe className="h-4 w-4 mr-1" />}
98
+ <span>{currentLabel}</span>
99
+ </Button>
100
+ </DropdownMenuTrigger>
101
+ <DropdownMenuContent align="end">
102
+ {locales.map((code) => (
103
+ <DropdownMenuItem
104
+ key={code}
105
+ onClick={() => onChange(code)}
106
+ className={code === locale ? 'bg-accent' : ''}
107
+ >
108
+ {getLabel(code)}
109
+ </DropdownMenuItem>
110
+ ))}
111
+ </DropdownMenuContent>
112
+ </DropdownMenu>
113
+ );
114
+ }
@@ -2,6 +2,9 @@
2
2
  * Shared Layout Components
3
3
  */
4
4
 
5
+ export { LocaleSwitcher } from './LocaleSwitcher';
6
+ export type { LocaleSwitcherProps } from './LocaleSwitcher';
7
+
5
8
  export { UserMenu } from './UserMenu';
6
9
  export type { UserMenuProps } from './UserMenu';
7
10
 
@@ -11,8 +11,8 @@
11
11
  export * from './types';
12
12
 
13
13
  // Shared components
14
- export { UserMenu } from './_components';
15
- export type { UserMenuProps } from './_components';
14
+ export { LocaleSwitcher, UserMenu } from './_components';
15
+ export type { LocaleSwitcherProps, UserMenuProps } from './_components';
16
16
 
17
17
  // Smart layout router
18
18
  export * from './AppLayout';