@djangocfg/layouts 2.1.111 → 2.1.112
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 +69 -0
- package/package.json +14 -14
- package/src/layouts/AppLayout/AppLayout.tsx +42 -17
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +8 -1
- package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +14 -1
- package/src/layouts/PublicLayout/PublicLayout.tsx +6 -0
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +14 -0
- package/src/layouts/_components/LocaleSwitcher.tsx +114 -0
- package/src/layouts/_components/index.ts +3 -0
- package/src/layouts/index.ts +2 -2
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.
|
|
3
|
+
"version": "2.1.112",
|
|
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.
|
|
78
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
79
|
-
"@djangocfg/i18n": "^2.1.
|
|
80
|
-
"@djangocfg/ui-core": "^2.1.
|
|
81
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
82
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
77
|
+
"@djangocfg/api": "^2.1.112",
|
|
78
|
+
"@djangocfg/centrifugo": "^2.1.112",
|
|
79
|
+
"@djangocfg/i18n": "^2.1.112",
|
|
80
|
+
"@djangocfg/ui-core": "^2.1.112",
|
|
81
|
+
"@djangocfg/ui-nextjs": "^2.1.112",
|
|
82
|
+
"@djangocfg/ui-tools": "^2.1.112",
|
|
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.
|
|
106
|
-
"@djangocfg/i18n": "^2.1.
|
|
107
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
108
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
109
|
-
"@djangocfg/ui-core": "^2.1.
|
|
110
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
111
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
105
|
+
"@djangocfg/api": "^2.1.112",
|
|
106
|
+
"@djangocfg/i18n": "^2.1.112",
|
|
107
|
+
"@djangocfg/centrifugo": "^2.1.112",
|
|
108
|
+
"@djangocfg/typescript-config": "^2.1.112",
|
|
109
|
+
"@djangocfg/ui-core": "^2.1.112",
|
|
110
|
+
"@djangocfg/ui-nextjs": "^2.1.112",
|
|
111
|
+
"@djangocfg/ui-tools": "^2.1.112",
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) &&
|
|
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
|
+
}
|
package/src/layouts/index.ts
CHANGED
|
@@ -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';
|