@djangocfg/layouts 2.1.303 → 2.1.307

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.
Files changed (29) hide show
  1. package/package.json +18 -18
  2. package/src/layouts/AppLayout/AppLayout.tsx +14 -11
  3. package/src/layouts/AppLayout/BaseApp.tsx +3 -1
  4. package/src/layouts/AppLayout/LayoutI18nProvider.tsx +59 -0
  5. package/src/layouts/AppLayout/index.ts +7 -0
  6. package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -5
  7. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +2 -4
  8. package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +6 -11
  9. package/src/layouts/PublicLayout/footers/DefaultFooter/types.ts +15 -6
  10. package/src/layouts/PublicLayout/navbars/FloatingNavbar/FloatingNavbar.tsx +1 -5
  11. package/src/layouts/PublicLayout/navbars/FlushNavbar/FlushNavbar.tsx +1 -5
  12. package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +1 -6
  13. package/src/layouts/PublicLayout/primitives/NavControls.tsx +5 -9
  14. package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +2 -6
  15. package/src/layouts/PublicLayout/shared/NavbarShell.tsx +1 -7
  16. package/src/layouts/_components/LocaleSwitcher.tsx +40 -178
  17. package/src/layouts/_components/PrivateSidebarAccount.tsx +6 -8
  18. package/src/layouts/_components/UserMenu.tsx +15 -19
  19. package/src/layouts/_components/index.ts +23 -2
  20. package/src/layouts/_components/locale-switcher/LocaleCard.tsx +91 -0
  21. package/src/layouts/_components/locale-switcher/LocaleGrid.tsx +128 -0
  22. package/src/layouts/_components/locale-switcher/LocaleSwitcher.tsx +100 -0
  23. package/src/layouts/_components/locale-switcher/LocaleSwitcherDialog.tsx +168 -0
  24. package/src/layouts/_components/locale-switcher/LocaleSwitcherDropdown.tsx +85 -0
  25. package/src/layouts/_components/locale-switcher/LocaleSwitcherTrigger.tsx +74 -0
  26. package/src/layouts/_components/locale-switcher/index.ts +27 -0
  27. package/src/layouts/_components/locale-switcher/localeMeta.ts +109 -0
  28. package/src/layouts/_components/locale-switcher/types.ts +48 -0
  29. package/src/layouts/types/layout.types.ts +37 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.303",
3
+ "version": "2.1.307",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -74,14 +74,14 @@
74
74
  "check": "tsc --noEmit"
75
75
  },
76
76
  "peerDependencies": {
77
- "@djangocfg/api": "^2.1.303",
78
- "@djangocfg/centrifugo": "^2.1.303",
79
- "@djangocfg/debuger": "^2.1.303",
80
- "@djangocfg/i18n": "^2.1.303",
81
- "@djangocfg/monitor": "^2.1.303",
82
- "@djangocfg/ui-core": "^2.1.303",
83
- "@djangocfg/ui-nextjs": "^2.1.303",
84
- "@djangocfg/ui-tools": "^2.1.303",
77
+ "@djangocfg/api": "^2.1.307",
78
+ "@djangocfg/centrifugo": "^2.1.307",
79
+ "@djangocfg/debuger": "^2.1.307",
80
+ "@djangocfg/i18n": "^2.1.307",
81
+ "@djangocfg/monitor": "^2.1.307",
82
+ "@djangocfg/ui-core": "^2.1.307",
83
+ "@djangocfg/ui-nextjs": "^2.1.307",
84
+ "@djangocfg/ui-tools": "^2.1.307",
85
85
  "@hookform/resolvers": "^5.2.2",
86
86
  "consola": "^3.4.2",
87
87
  "lucide-react": "^0.545.0",
@@ -110,15 +110,15 @@
110
110
  "uuid": "^11.1.0"
111
111
  },
112
112
  "devDependencies": {
113
- "@djangocfg/api": "^2.1.303",
114
- "@djangocfg/centrifugo": "^2.1.303",
115
- "@djangocfg/debuger": "^2.1.303",
116
- "@djangocfg/i18n": "^2.1.303",
117
- "@djangocfg/monitor": "^2.1.303",
118
- "@djangocfg/typescript-config": "^2.1.303",
119
- "@djangocfg/ui-core": "^2.1.303",
120
- "@djangocfg/ui-nextjs": "^2.1.303",
121
- "@djangocfg/ui-tools": "^2.1.303",
113
+ "@djangocfg/api": "^2.1.307",
114
+ "@djangocfg/centrifugo": "^2.1.307",
115
+ "@djangocfg/debuger": "^2.1.307",
116
+ "@djangocfg/i18n": "^2.1.307",
117
+ "@djangocfg/monitor": "^2.1.307",
118
+ "@djangocfg/typescript-config": "^2.1.307",
119
+ "@djangocfg/ui-core": "^2.1.307",
120
+ "@djangocfg/ui-nextjs": "^2.1.307",
121
+ "@djangocfg/ui-tools": "^2.1.307",
122
122
  "@types/node": "^24.7.2",
123
123
  "@types/react": "^19.1.0",
124
124
  "@types/react-dom": "^19.1.0",
@@ -38,6 +38,7 @@ import { ClientOnly, Suspense } from '../../components/core';
38
38
  import { usePathnameWithoutLocale } from '../../hooks';
39
39
  import { matchesPath } from '../../utils/pathMatcher';
40
40
  import { BaseApp } from './BaseApp';
41
+ import { useLayoutI18nOptional } from './LayoutI18nProvider';
41
42
  import { ForcedThemeProvider, ThemeOverride, resolveForcedTheme } from '@djangocfg/ui-nextjs/theme';
42
43
 
43
44
  import type {
@@ -159,10 +160,12 @@ function determineLayoutMode(
159
160
  /**
160
161
  * Props passed to every layout component (`public` / `private` / `admin`).
161
162
  * Use `publicChrome` to pass defaults for `FloatingNavbar` / `DefaultFooter` from `AppLayout`.
163
+ *
164
+ * Locale plumbing is no longer threaded as a prop — layouts read it from
165
+ * `useLayoutI18nOptional()` (mounted in `BaseApp`).
162
166
  */
163
167
  export interface AppLayoutLayoutComponentProps {
164
168
  children: ReactNode;
165
- i18n?: I18nLayoutConfig;
166
169
  publicChrome?: AppLayoutPublicChrome;
167
170
  }
168
171
 
@@ -298,7 +301,6 @@ interface AppLayoutContentProps {
298
301
  adminLayout?: LayoutConfig;
299
302
  noLayoutPaths?: string | string[];
300
303
  authPath?: string;
301
- i18n?: I18nLayoutConfig;
302
304
  publicChrome?: AppLayoutPublicChrome;
303
305
  themeOverrides?: ThemeOverrideRule[];
304
306
  }
@@ -316,12 +318,13 @@ function AppLayoutContent({
316
318
  adminLayout,
317
319
  noLayoutPaths,
318
320
  authPath = '/auth',
319
- i18n,
320
321
  publicChrome,
321
322
  themeOverrides,
322
323
  }: AppLayoutContentProps) {
323
- // Use pathname without locale prefix for route matching.
324
- // Pass locale from i18n so strip is exactavoids regex misfiring on /ui, /ko, etc.
324
+ // Use pathname without locale prefix for route matching. The current
325
+ // locale comes from the LayoutI18nProvider mounted by BaseApppassing
326
+ // it explicitly avoids the regex misfiring on `/ui`, `/ko`, etc.
327
+ const i18n = useLayoutI18nOptional();
325
328
  const pathname = usePathnameWithoutLocale(i18n?.locale);
326
329
 
327
330
  // Merge authPath into noLayoutPaths — auth pages are always fullscreen
@@ -361,7 +364,7 @@ function AppLayoutContent({
361
364
  return (
362
365
  <ClientOnly>
363
366
  <Suspense>
364
- <privateLayout.component i18n={i18n} publicChrome={publicChrome}>
367
+ <privateLayout.component publicChrome={publicChrome}>
365
368
  {children}
366
369
  </privateLayout.component>
367
370
  </Suspense>
@@ -374,7 +377,7 @@ function AppLayoutContent({
374
377
  return (
375
378
  <ClientOnly>
376
379
  <Suspense>
377
- <adminLayout.component i18n={i18n} publicChrome={publicChrome}>
380
+ <adminLayout.component publicChrome={publicChrome}>
378
381
  {children}
379
382
  </adminLayout.component>
380
383
  </Suspense>
@@ -385,7 +388,7 @@ function AppLayoutContent({
385
388
  if (!privateLayout) {
386
389
  if (publicLayout) {
387
390
  return (
388
- <publicLayout.component i18n={i18n} publicChrome={publicChrome}>
391
+ <publicLayout.component publicChrome={publicChrome}>
389
392
  {children}
390
393
  </publicLayout.component>
391
394
  );
@@ -395,7 +398,7 @@ function AppLayoutContent({
395
398
  return (
396
399
  <ClientOnly>
397
400
  <Suspense>
398
- <privateLayout.component i18n={i18n} publicChrome={publicChrome}>
401
+ <privateLayout.component publicChrome={publicChrome}>
399
402
  {children}
400
403
  </privateLayout.component>
401
404
  </Suspense>
@@ -409,7 +412,7 @@ function AppLayoutContent({
409
412
  return children;
410
413
  }
411
414
  return (
412
- <publicLayout.component i18n={i18n} publicChrome={publicChrome}>
415
+ <publicLayout.component publicChrome={publicChrome}>
413
416
  {children}
414
417
  </publicLayout.component>
415
418
  );
@@ -480,6 +483,7 @@ export function AppLayout(props: AppLayoutProps) {
480
483
  pwaInstall={pwaInstall}
481
484
  monitor={monitor}
482
485
  debug={debug}
486
+ i18n={i18n}
483
487
  >
484
488
  <AppLayoutContent
485
489
  children={children}
@@ -488,7 +492,6 @@ export function AppLayout(props: AppLayoutProps) {
488
492
  adminLayout={adminLayout}
489
493
  noLayoutPaths={noLayoutPaths}
490
494
  authPath={authPath}
491
- i18n={i18n}
492
495
  publicChrome={publicChrome}
493
496
  themeOverrides={themeOverrides}
494
497
  />
@@ -58,6 +58,7 @@ import { AuthDialog } from '../../snippets/AuthDialog';
58
58
  import { A2HSHint, PWAPageResumeManager, PwaProvider } from '@djangocfg/ui-nextjs/pwa';
59
59
 
60
60
  import type { BaseLayoutProps } from '../types/layout.types';
61
+ import { LayoutI18nProvider } from './LayoutI18nProvider';
61
62
 
62
63
  // Lazy load DebugButton from @djangocfg/debuger
63
64
  const DebugButton = dynamic(
@@ -89,6 +90,7 @@ export function BaseApp({
89
90
  pwaInstall,
90
91
  monitor,
91
92
  debug,
93
+ i18n,
92
94
  }: BaseAppProps) {
93
95
  // ErrorBoundary is enabled by default
94
96
  const enableErrorBoundary = errorBoundary?.enabled !== false;
@@ -156,7 +158,7 @@ export function BaseApp({
156
158
  onMonitorCapture={(d) => FrontendMonitor.capture(errorDetailToMonitorEvent(d))}
157
159
  >
158
160
  <MonitorProvider {...monitorConfig} />
159
- {children}
161
+ <LayoutI18nProvider value={i18n}>{children}</LayoutI18nProvider>
160
162
  <NextTopLoader
161
163
  color="hsl(var(--primary))"
162
164
  height={3}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * LayoutI18nProvider — single source of truth for locale plumbing inside layouts.
3
+ *
4
+ * Mounted by `BaseApp` when `i18n` is passed. Every locale-aware component
5
+ * inside layouts (footer, navbar controls, mobile drawer, private sidebar
6
+ * account, UserMenu, LocaleSwitcher) reads from here instead of receiving
7
+ * `i18n` as a prop.
8
+ *
9
+ * Components that may render outside the provider (e.g. raw shells used in
10
+ * playground previews) should call `useLayoutI18nOptional()` and gracefully
11
+ * hide locale-related UI when it returns `null`.
12
+ */
13
+
14
+ 'use client';
15
+
16
+ import * as React from 'react';
17
+
18
+ import type { I18nLayoutConfig } from '../types/layout.types';
19
+
20
+ const LayoutI18nContext = React.createContext<I18nLayoutConfig | null>(null);
21
+
22
+ export interface LayoutI18nProviderProps {
23
+ value: I18nLayoutConfig | null | undefined;
24
+ children: React.ReactNode;
25
+ }
26
+
27
+ /**
28
+ * Provider — mount once at the app root (handled automatically by `BaseApp`).
29
+ * Passing `null`/`undefined` is fine: consumers fall back to "no locale switcher".
30
+ */
31
+ export function LayoutI18nProvider({ value, children }: LayoutI18nProviderProps) {
32
+ // `null` keeps the context discriminator simple — `useLayoutI18nOptional`
33
+ // returns `null` whether the provider is missing or `value` is empty.
34
+ const stable = value ?? null;
35
+ return <LayoutI18nContext.Provider value={stable}>{children}</LayoutI18nContext.Provider>;
36
+ }
37
+
38
+ /**
39
+ * Throws when used outside the provider. Use for components that strictly
40
+ * require locale plumbing (the locale picker dialog, for instance).
41
+ */
42
+ export function useLayoutI18n(): I18nLayoutConfig {
43
+ const ctx = React.useContext(LayoutI18nContext);
44
+ if (!ctx) {
45
+ throw new Error(
46
+ 'useLayoutI18n must be used inside <LayoutI18nProvider> — pass `i18n` to <BaseApp> / <AppLayout>.',
47
+ );
48
+ }
49
+ return ctx;
50
+ }
51
+
52
+ /**
53
+ * Optional accessor — returns `null` outside the provider or when no `i18n`
54
+ * was configured. Use for opt-in mounts (footer locale switcher, navbar
55
+ * locale dropdown) that should silently disappear when locale support is off.
56
+ */
57
+ export function useLayoutI18nOptional(): I18nLayoutConfig | null {
58
+ return React.useContext(LayoutI18nContext);
59
+ }
@@ -20,6 +20,13 @@ export { mergeAppLayoutPublicChrome } from './AppLayout';
20
20
  export { BaseApp } from './BaseApp';
21
21
  export type { BaseAppProps } from './BaseApp';
22
22
 
23
+ export {
24
+ LayoutI18nProvider,
25
+ useLayoutI18n,
26
+ useLayoutI18nOptional,
27
+ } from './LayoutI18nProvider';
28
+ export type { LayoutI18nProviderProps } from './LayoutI18nProvider';
29
+
23
30
  // Re-export theme-override primitives so consumers can import both the
24
31
  // AppLayout config surface and the rule/theme types from one place.
25
32
  export {
@@ -16,7 +16,7 @@ import { Preloader } from '@djangocfg/ui-core/components';
16
16
  import { SidebarInset, SidebarProvider } from '@djangocfg/ui-nextjs/components';
17
17
 
18
18
  import type { AppLayoutPublicChrome } from '../AppLayout/AppLayout';
19
- import type { I18nLayoutConfig, LayoutVisualConfig } from '../types';
19
+ import type { LayoutVisualConfig } from '../types';
20
20
  import { UserMenuConfig } from '../types';
21
21
  import { PrivateContent, PrivateSidebar } from './components';
22
22
 
@@ -101,8 +101,6 @@ export interface PrivateLayoutProps {
101
101
  pathname?: string;
102
102
  /** Content padding */
103
103
  contentPadding?: 'none' | 'default';
104
- /** i18n configuration for locale switching */
105
- i18n?: I18nLayoutConfig;
106
104
  /**
107
105
  * Visual style of the shell. Defaults to `'boxed'` (inset rounded card on a
108
106
  * sidebar-coloured canvas). Pass `{ variant: 'full-bleed' }` for the legacy
@@ -124,7 +122,6 @@ export function PrivateLayout({
124
122
  header,
125
123
  pathname,
126
124
  contentPadding = 'default',
127
- i18n,
128
125
  visual,
129
126
  requireAuth = true,
130
127
  }: PrivateLayoutProps) {
@@ -167,7 +164,6 @@ export function PrivateLayout({
167
164
  <PrivateSidebar
168
165
  sidebar={sidebar}
169
166
  header={header}
170
- i18n={i18n}
171
167
  pathname={pathname}
172
168
  variant={sidebarVariant}
173
169
  />
@@ -29,7 +29,6 @@ import { cn } from '@djangocfg/ui-core/lib';
29
29
  import { PrivateSidebarAccount } from '../../_components/PrivateSidebarAccount';
30
30
  import { LucideIcon } from '../../../components';
31
31
 
32
- import type { I18nLayoutConfig } from '../../types';
33
32
  import type { HeaderConfig, SidebarItem, SidebarConfig } from '../PrivateLayout';
34
33
 
35
34
  /** Few items → roomier rows; many items → tighter. Same breakpoints for demo, CarAPIS, etc. */
@@ -99,7 +98,6 @@ const RAIL_NAV = DENSITY.default;
99
98
  interface PrivateSidebarProps {
100
99
  sidebar: SidebarConfig;
101
100
  header?: HeaderConfig;
102
- i18n?: I18nLayoutConfig;
103
101
  pathname?: string;
104
102
  /**
105
103
  * shadcn-sidebar `variant`. Used to trigger the inset/boxed visual:
@@ -109,7 +107,7 @@ interface PrivateSidebarProps {
109
107
  variant?: 'sidebar' | 'inset';
110
108
  }
111
109
 
112
- export function PrivateSidebar({ sidebar, header, i18n, pathname: pathnameProp, variant = 'sidebar' }: PrivateSidebarProps) {
110
+ export function PrivateSidebar({ sidebar, header, pathname: pathnameProp, variant = 'sidebar' }: PrivateSidebarProps) {
113
111
  const pathnameFromNext = useNextPathname();
114
112
  const pathname = pathnameProp ?? pathnameFromNext;
115
113
  const { state, isMobile, setOpen, setOpenMobile } = useSidebar();
@@ -324,7 +322,7 @@ export function PrivateSidebar({ sidebar, header, i18n, pathname: pathnameProp,
324
322
 
325
323
  <SidebarFooter className="p-2">
326
324
  {footerExtra}
327
- <PrivateSidebarAccount header={header} i18n={i18n} />
325
+ <PrivateSidebarAccount header={header} />
328
326
  </SidebarFooter>
329
327
  </Sidebar>
330
328
  );
@@ -15,6 +15,7 @@ import { useForcedTheme, useThemeContext } from '@djangocfg/ui-nextjs/theme';
15
15
  import { Link } from '@djangocfg/ui-core/components';
16
16
 
17
17
  import { LocaleSwitcher } from '../../../_components/LocaleSwitcher';
18
+ import { useLayoutI18nOptional } from '../../../AppLayout/LayoutI18nProvider';
18
19
  import { FooterMenuSections } from './FooterMenuSections';
19
20
  import { FooterProjectInfo } from './FooterProjectInfo';
20
21
 
@@ -117,7 +118,7 @@ export function DefaultFooter({ config }: DefaultFooterProps) {
117
118
  const socialLinks = config.social;
118
119
  const copyrightProp = config.meta?.copyright;
119
120
  const creditsProp = config.meta?.credits;
120
- const i18n = config.i18n;
121
+ const i18n = useLayoutI18nOptional();
121
122
  const showThemeSwitcher = config.controls?.showThemeSwitcher !== false;
122
123
  const showLocaleSwitcher =
123
124
  config.controls?.showLocaleSwitcher !== false && Boolean(i18n);
@@ -260,12 +261,9 @@ export function DefaultFooter({ config }: DefaultFooterProps) {
260
261
  {showFooterControlsRow && (
261
262
  <div className="mt-5 flex items-center justify-center gap-2 border-t border-border/60 pt-4">
262
263
  {showThemeSwitcher && <ThemeModeControl />}
263
- {showLocaleSwitcher && i18n && (
264
+ {showLocaleSwitcher && (
264
265
  <LocaleSwitcher
265
- locale={i18n.locale}
266
- locales={i18n.locales}
267
- onChange={i18n.onLocaleChange}
268
- variant="outline"
266
+ buttonVariant="outline"
269
267
  size="default"
270
268
  className="h-10 rounded-full border-border/60 bg-muted/30 text-sm hover:bg-muted/40"
271
269
  />
@@ -348,12 +346,9 @@ export function DefaultFooter({ config }: DefaultFooterProps) {
348
346
  <div className="flex items-center justify-center gap-2 lg:justify-self-end">
349
347
  {slots?.bottomEnd}
350
348
  {showThemeSwitcher && <ThemeModeControl />}
351
- {showLocaleSwitcher && i18n && (
349
+ {showLocaleSwitcher && (
352
350
  <LocaleSwitcher
353
- locale={i18n.locale}
354
- locales={i18n.locales}
355
- onChange={i18n.onLocaleChange}
356
- variant="outline"
351
+ buttonVariant="outline"
357
352
  size="default"
358
353
  className="h-10 rounded-full border-border/60 bg-muted/30 text-sm hover:bg-muted/40"
359
354
  />
@@ -5,7 +5,7 @@
5
5
  import type { LucideIcon } from 'lucide-react';
6
6
  import type { ReactNode } from 'react';
7
7
 
8
- import type { FooterLink, FooterMenuSection, FooterSocialLinks, I18nLayoutConfig } from '../../../types';
8
+ import type { FooterLink, FooterMenuSection, FooterSocialLinks } from '../../../types';
9
9
 
10
10
  export type { FooterLink, FooterMenuSection, FooterSocialLinks };
11
11
 
@@ -23,6 +23,15 @@ export interface DefaultFooterConfig {
23
23
  icon: LucideIcon;
24
24
  text: string;
25
25
  };
26
+ /**
27
+ * Logo node displayed in the locale-switcher dialog header.
28
+ * Falls back to a generic Globe glyph when omitted.
29
+ */
30
+ logo?: ReactNode;
31
+ /**
32
+ * Project name displayed beside the logo in the locale-switcher dialog.
33
+ */
34
+ name?: string;
26
35
  };
27
36
  menus?: {
28
37
  sections?: FooterMenuSection[];
@@ -38,16 +47,16 @@ export interface DefaultFooterConfig {
38
47
  };
39
48
  };
40
49
  social?: FooterSocialLinks;
41
- i18n?: I18nLayoutConfig;
42
50
  /**
43
- * Full (`variant: 'full'`) footer only — bottom row with theme and/or locale controls.
44
- * - Locale: still requires `i18n`; omit `i18n` to hide the language switcher, or set
45
- * `showLocaleSwitcher: false` when you pass `i18n` but want the switcher elsewhere.
51
+ * Full (`variant: 'full'`) footer only — bottom row with theme and/or locale
52
+ * controls. The locale switcher reads its plumbing from `LayoutI18nProvider`
53
+ * (mounted by `BaseApp`); when the provider is empty, the switcher hides
54
+ * itself regardless of this flag.
46
55
  */
47
56
  controls?: {
48
57
  /** Light / system / dark toggle. @default true */
49
58
  showThemeSwitcher?: boolean;
50
- /** Requires `i18n`. @default true */
59
+ /** @default true */
51
60
  showLocaleSwitcher?: boolean;
52
61
  };
53
62
  /**
@@ -21,7 +21,7 @@ import type {
21
21
  PublicNavbarShellConfig,
22
22
  PublicNavLayout,
23
23
  } from '../../navbarTypes';
24
- import type { I18nLayoutConfig, NavigationItem, UserMenuConfig } from '../../../types';
24
+ import type { NavigationItem, UserMenuConfig } from '../../../types';
25
25
 
26
26
  import { FloatingMobileDrawer } from './FloatingMobileDrawer';
27
27
 
@@ -65,8 +65,6 @@ export interface FloatingNavbarConfig {
65
65
  /** Locale dropdown. Requires `i18n`. @default false */
66
66
  showLocaleSwitcher?: boolean;
67
67
  };
68
- /** i18n config (current locale + locales + onLocaleChange). */
69
- i18n?: I18nLayoutConfig;
70
68
  }
71
69
 
72
70
  export interface FloatingNavbarProps {
@@ -113,7 +111,6 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
113
111
  actionsLeadingSlot={config.actionsLeadingSlot}
114
112
  actionsTrailingSlot={config.actionsTrailingSlot}
115
113
  controls={config.controls}
116
- i18n={config.i18n}
117
114
  outerClassName={outerClassName}
118
115
  shapeClassName={shapeClassName}
119
116
  shapeForState={({ scrolled, transparent }) =>
@@ -131,7 +128,6 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
131
128
  containerClassName={containerClassName}
132
129
  rounding={rounding}
133
130
  controls={config.controls}
134
- i18n={config.i18n}
135
131
  />
136
132
  </>
137
133
  );
@@ -20,7 +20,7 @@ import type {
20
20
  PublicNavbarShellConfig,
21
21
  PublicNavLayout,
22
22
  } from '../../navbarTypes';
23
- import type { I18nLayoutConfig, NavigationItem, UserMenuConfig } from '../../../types';
23
+ import type { NavigationItem, UserMenuConfig } from '../../../types';
24
24
 
25
25
  import { FlushMobileDrawer } from './FlushMobileDrawer';
26
26
 
@@ -64,8 +64,6 @@ export interface FlushNavbarConfig {
64
64
  /** Locale dropdown. Requires `i18n`. @default false */
65
65
  showLocaleSwitcher?: boolean;
66
66
  };
67
- /** i18n config (current locale + locales + onLocaleChange). */
68
- i18n?: I18nLayoutConfig;
69
67
  }
70
68
 
71
69
  export interface FlushNavbarProps {
@@ -109,7 +107,6 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
109
107
  actionsLeadingSlot={config.actionsLeadingSlot}
110
108
  actionsTrailingSlot={config.actionsTrailingSlot}
111
109
  controls={config.controls}
112
- i18n={config.i18n}
113
110
  outerClassName={outerClassName}
114
111
  shapeClassName={shapeClassName}
115
112
  shapeForState={({ scrolled, transparent }) =>
@@ -126,7 +123,6 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
126
123
  userMenu={config.userMenu}
127
124
  containerClassName={containerClassName}
128
125
  controls={config.controls}
129
- i18n={config.i18n}
130
126
  />
131
127
  </>
132
128
  );
@@ -26,7 +26,7 @@ import type {
26
26
  PublicNavbarPosition,
27
27
  PublicNavLayout,
28
28
  } from '../../navbarTypes';
29
- import type { I18nLayoutConfig, NavigationItem, UserMenuConfig } from '../../../types';
29
+ import type { NavigationItem, UserMenuConfig } from '../../../types';
30
30
 
31
31
  import { MinimalMobileDrawer } from './MinimalMobileDrawer';
32
32
 
@@ -81,8 +81,6 @@ export interface MinimalNavbarConfig {
81
81
  /** Locale dropdown. Requires `i18n`. @default false */
82
82
  showLocaleSwitcher?: boolean;
83
83
  };
84
- /** i18n config (current locale + locales + onLocaleChange). */
85
- i18n?: I18nLayoutConfig;
86
84
  }
87
85
 
88
86
  export interface MinimalNavbarProps {
@@ -115,7 +113,6 @@ function MinimalActions({
115
113
  variant="desktop"
116
114
  groups={ctx.userMenu?.groups}
117
115
  authPath={ctx.userMenu?.authPath}
118
- i18n={ctx.userMenu?.i18n}
119
116
  />
120
117
  </div>
121
118
 
@@ -168,7 +165,6 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
168
165
  transparent={transparent}
169
166
  transparentThreshold={config.transparentThreshold}
170
167
  controls={config.controls}
171
- i18n={config.i18n}
172
168
  outerClassName={outerClassName}
173
169
  shapeClassName={shapeClassName}
174
170
  innerPadding={containerClassName}
@@ -187,7 +183,6 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
187
183
  userMenu={config.userMenu}
188
184
  containerClassName={containerClassName}
189
185
  controls={config.controls}
190
- i18n={config.i18n}
191
186
  />
192
187
  </>
193
188
  );
@@ -7,12 +7,10 @@ import { Button } from '@djangocfg/ui-core/components';
7
7
  import { cn } from '@djangocfg/ui-core/lib';
8
8
  import { useThemeContext } from '@djangocfg/ui-nextjs/theme';
9
9
 
10
- import type { I18nLayoutConfig } from '../../types';
11
10
  import { LocaleSwitcher } from '../../_components/LocaleSwitcher';
11
+ import { useLayoutI18nOptional } from '../../AppLayout/LayoutI18nProvider';
12
12
 
13
13
  export interface NavControlsProps {
14
- /** Optional i18n config. Required to render the locale switcher. */
15
- i18n?: I18nLayoutConfig;
16
14
  /** Show the theme (light / system / dark) pill. @default false */
17
15
  showThemeSwitcher?: boolean;
18
16
  /** Show the locale dropdown. Requires `i18n`. @default false */
@@ -81,12 +79,12 @@ function ThemeModeControl({ size }: { size: 'compact' | 'default' }) {
81
79
  * to a navbar config.
82
80
  */
83
81
  export function NavControls({
84
- i18n,
85
82
  showThemeSwitcher = false,
86
83
  showLocaleSwitcher = false,
87
84
  size = 'compact',
88
85
  className,
89
86
  }: NavControlsProps) {
87
+ const i18n = useLayoutI18nOptional();
90
88
  const renderLocale = showLocaleSwitcher && Boolean(i18n);
91
89
  if (!showThemeSwitcher && !renderLocale) return null;
92
90
 
@@ -98,12 +96,10 @@ export function NavControls({
98
96
  return (
99
97
  <div className={cn('inline-flex items-center gap-1.5', className)}>
100
98
  {showThemeSwitcher && <ThemeModeControl size={size} />}
101
- {renderLocale && i18n && (
99
+ {renderLocale && (
102
100
  <LocaleSwitcher
103
- locale={i18n.locale}
104
- locales={i18n.locales}
105
- onChange={i18n.onLocaleChange}
106
- variant="outline"
101
+ variant="dialog"
102
+ buttonVariant="outline"
107
103
  size={size === 'compact' ? 'sm' : 'default'}
108
104
  showTriggerLabel={false}
109
105
  className={localeBtnClass}
@@ -22,7 +22,7 @@ import { usePublicLayoutOptional } from '../context';
22
22
  import { useMobileNavPanel } from '../hooks';
23
23
  import { NavControls } from '../primitives/NavControls';
24
24
 
25
- import type { I18nLayoutConfig, NavigationItem, UserMenuConfig } from '../../types';
25
+ import type { NavigationItem, UserMenuConfig } from '../../types';
26
26
 
27
27
  export interface MobileDrawerShellProps {
28
28
  isOpen?: boolean;
@@ -38,8 +38,6 @@ export interface MobileDrawerShellProps {
38
38
  showThemeSwitcher?: boolean;
39
39
  showLocaleSwitcher?: boolean;
40
40
  };
41
- /** i18n config — required for the locale switcher row. */
42
- i18n?: I18nLayoutConfig;
43
41
  }
44
42
 
45
43
  export function MobileDrawerShell(props: MobileDrawerShellProps) {
@@ -81,8 +79,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
81
79
  const hasSessionUser = Boolean(isAuthenticated && user);
82
80
  const showSignInFooter = !hasSessionUser;
83
81
  const showThemeSwitcher = props.controls?.showThemeSwitcher === true;
84
- const showLocaleSwitcher =
85
- props.controls?.showLocaleSwitcher === true && Boolean(props.i18n);
82
+ const showLocaleSwitcher = props.controls?.showLocaleSwitcher === true;
86
83
  const showControlsRow = showThemeSwitcher || showLocaleSwitcher;
87
84
 
88
85
  return (
@@ -199,7 +196,6 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
199
196
  {showControlsRow && (
200
197
  <div className="shrink-0 border-t border-border/50 px-4 py-3 flex items-center justify-center gap-2">
201
198
  <NavControls
202
- i18n={props.i18n}
203
199
  showThemeSwitcher={showThemeSwitcher}
204
200
  showLocaleSwitcher={showLocaleSwitcher}
205
201
  size="default"
@@ -50,8 +50,6 @@ import { NavBrand } from '../primitives/NavBrand';
50
50
  import { NavControls } from '../primitives/NavControls';
51
51
  import { NavDesktopItems } from '../primitives/NavDesktopItems';
52
52
 
53
- import type { I18nLayoutConfig } from '../../types';
54
-
55
53
  const heightCls: Record<PublicNavbarHeight, string> = {
56
54
  sm: 'py-2',
57
55
  md: 'py-3.5',
@@ -136,8 +134,6 @@ export interface NavbarShellProps {
136
134
  actionsTrailingSlot?: ReactNode;
137
135
 
138
136
  // ── Theme + locale controls (rendered next to UserMenu on desktop) ────────
139
- /** i18n config — enables the locale switcher. Same type as `DefaultFooter`. */
140
- i18n?: I18nLayoutConfig;
141
137
  /** Toggle individual controls. Locale switcher also requires `i18n`. */
142
138
  controls?: {
143
139
  /** Light / system / dark pill. @default false */
@@ -267,12 +263,10 @@ export function NavbarShell(props: NavbarShellProps) {
267
263
  ) : null;
268
264
 
269
265
  const showThemeSwitcher = props.controls?.showThemeSwitcher === true;
270
- const showLocaleSwitcher =
271
- props.controls?.showLocaleSwitcher === true && Boolean(props.i18n);
266
+ const showLocaleSwitcher = props.controls?.showLocaleSwitcher === true;
272
267
  const hasControls = showThemeSwitcher || showLocaleSwitcher;
273
268
  const controlsNode = hasControls ? (
274
269
  <NavControls
275
- i18n={props.i18n}
276
270
  showThemeSwitcher={showThemeSwitcher}
277
271
  showLocaleSwitcher={showLocaleSwitcher}
278
272
  />