@djangocfg/nextjs 2.1.626 → 2.1.627

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
@@ -304,6 +304,16 @@ function MyComponent() {
304
304
  }
305
305
  ```
306
306
 
307
+ #### Shared locale cookie (several hosts, one family)
308
+
309
+ `NEXT_LOCALE` is written by the **proxy** (`createProxy` → `shareLocaleCookie`) and by `useChangeLocale` in the browser, never during render. Server components read the locale from the URL segment, so the cookie cannot change what SSR renders; it only decides where a bare `/` redirects. With `Domain=<registrable domain>` every host of the family sends the same value; the host-only twin is expired so it cannot shadow it. Override the domain guess with `NEXT_PUBLIC_COOKIE_DOMAIN`.
310
+
311
+ What it costs, and the rules that keep SSR intact:
312
+ - A response that sets a cookie is not cached by a CDN by default. The proxy sets it only when next-intl wrote one (a bare-path visit or a switch), not on every page view; keep it that way and do not add `Set-Cookie` to static pages.
313
+ - Never read `NEXT_LOCALE` in a server component (`cookies()` makes the route dynamic). Take `locale` from `params`.
314
+ - SEO sites (one language per host, indexed) keep `localeCookie: false`; sharing applies only to app families behind a login.
315
+ - If this ever causes trouble, the fallback is no cookie at all: the locale lives in the URL, and the family switcher links to the sibling host with the same locale prefix.
316
+
307
317
  #### Locale Switcher
308
318
 
309
319
  Smart wrapper around `@djangocfg/layouts` LocaleSwitcher with automatic next-intl hooks:
@@ -95,6 +95,49 @@ function NextIntlProvider({
95
95
  );
96
96
  }
97
97
 
98
+ // src/i18n/shared-locale.ts
99
+ var LOCALE_COOKIE = "NEXT_LOCALE";
100
+ var LOCALE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
101
+ var TWO_LABEL_SUFFIXES = /* @__PURE__ */ new Set([
102
+ "co.uk",
103
+ "org.uk",
104
+ "ac.uk",
105
+ "com.au",
106
+ "net.au",
107
+ "co.nz",
108
+ "co.jp",
109
+ "co.kr",
110
+ "or.kr",
111
+ "com.br",
112
+ "com.tr",
113
+ "com.cn",
114
+ "com.mx",
115
+ "co.in",
116
+ "co.za",
117
+ "com.sg",
118
+ "com.hk",
119
+ "com.tw",
120
+ "com.ar",
121
+ "com.ua",
122
+ "co.il"
123
+ ]);
124
+ function sharedCookieDomain(hostname, override) {
125
+ const forced = override?.trim().replace(/^\./, "");
126
+ if (forced) return forced;
127
+ const host = hostname.split(":")[0].toLowerCase();
128
+ if (!host || /^\d+(\.\d+){3}$/.test(host) || host.includes(":")) return null;
129
+ const labels = host.split(".");
130
+ if (labels.length < 2) return null;
131
+ const keep = TWO_LABEL_SUFFIXES.has(labels.slice(-2).join(".")) ? 3 : 2;
132
+ return labels.slice(-keep).join(".");
133
+ }
134
+ function localeCookieHeader(locale, domain, secure) {
135
+ return `${LOCALE_COOKIE}=${encodeURIComponent(locale)}; Path=/; Max-Age=${LOCALE_COOKIE_MAX_AGE}; SameSite=Lax${domain ? `; Domain=${domain}` : ""}${secure ? "; Secure" : ""}`;
136
+ }
137
+ function expireHostOnlyLocaleCookie(secure) {
138
+ return `${LOCALE_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax${secure ? "; Secure" : ""}`;
139
+ }
140
+
98
141
  // src/i18n/client.ts
99
142
  function useTranslations(namespace) {
100
143
  return useNextIntlTranslations(namespace);
@@ -128,9 +171,10 @@ function useDefaultLocale() {
128
171
  function useChangeLocale() {
129
172
  const pathname = usePathname();
130
173
  return (locale) => {
131
- const date = /* @__PURE__ */ new Date();
132
- date.setFullYear(date.getFullYear() + 1);
133
- document.cookie = `NEXT_LOCALE=${locale}; expires=${date.toUTCString()}; path=/`;
174
+ const secure = window.location.protocol === "https:";
175
+ const domain = sharedCookieDomain(window.location.hostname, process.env.NEXT_PUBLIC_COOKIE_DOMAIN);
176
+ document.cookie = localeCookieHeader(locale, domain, secure);
177
+ if (domain) document.cookie = expireHostOnlyLocaleCookie(secure);
134
178
  window.location.href = `/${locale}${pathname}`;
135
179
  };
136
180
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/i18n/client.ts","../../src/i18n/navigation.ts","../../src/i18n/routing.ts","../../src/i18n/provider.tsx"],"sourcesContent":["/**\n * Client-side i18n Hooks\n *\n * For use in Client Components ('use client')\n *\n * @example\n * ```tsx\n * 'use client';\n *\n * import { useTranslations, useLocale } from '@djangocfg/nextjs/i18n/client';\n *\n * export function MyComponent() {\n * const t = useTranslations('HomePage');\n * const locale = useLocale();\n *\n * return <h1>{t('title')}</h1>;\n * }\n * ```\n */\n\n'use client';\n\nimport {\n useFormatter, useLocale as useNextIntlLocale, useMessages, useNow, useTimeZone,\n useTranslations as useNextIntlTranslations\n} from 'next-intl';\n\nimport { usePathname } from './navigation';\nimport { I18nProvider, NextIntlProvider, useDeclaredLocales } from './provider';\nimport { routing } from './routing';\n\n// Re-export providers\nexport { I18nProvider, NextIntlProvider };\n\nimport type { LocaleCode } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Core Client Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Get translations in Client Components\n *\n * @example\n * ```tsx\n * const t = useTranslations('HomePage');\n * return <h1>{t('title')}</h1>;\n *\n * // With interpolation\n * return <p>{t('greeting', { name: 'John' })}</p>;\n * ```\n */\nexport function useTranslations<Namespace extends string = never>(\n namespace?: Namespace\n) {\n return useNextIntlTranslations(namespace);\n}\n\n/**\n * Get current locale in Client Components\n */\nexport function useLocale(): LocaleCode {\n return useNextIntlLocale() as LocaleCode;\n}\n\n/**\n * Get all messages\n * Useful for passing to child providers\n */\nexport { useMessages };\n\n/**\n * Get current time\n */\nexport { useNow };\n\n/**\n * Get timezone\n */\nexport { useTimeZone };\n\n/**\n * Get formatter for dates, numbers, etc.\n */\nexport { useFormatter };\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Convenience Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Shorthand for useTranslations\n * Alias for compatibility with @djangocfg/i18n\n */\nexport const useT = useTranslations;\n\n/**\n * Get namespaced translations\n *\n * @example\n * ```tsx\n * const pt = useNamespacedTranslations('payments');\n * return <span>{pt('balance.available')}</span>;\n * ```\n */\nexport function useNamespacedTranslations(namespace: string) {\n return useNextIntlTranslations(namespace);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Formatting Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Format a date according to locale\n *\n * @example\n * ```tsx\n * const formatDate = useDateFormatter();\n * return <span>{formatDate(new Date())}</span>;\n * ```\n */\nexport function useDateFormatter() {\n const formatter = useFormatter();\n return (date: Date | number, options?: Parameters<typeof formatter.dateTime>[1]) =>\n formatter.dateTime(date, options);\n}\n\n/**\n * Format a number according to locale\n *\n * @example\n * ```tsx\n * const formatNumber = useNumberFormatter();\n * return <span>{formatNumber(1234.56)}</span>;\n * ```\n */\nexport function useNumberFormatter() {\n const formatter = useFormatter();\n return (number: number, options?: Parameters<typeof formatter.number>[1]) =>\n formatter.number(number, options);\n}\n\n/**\n * Format relative time\n *\n * @example\n * ```tsx\n * const formatRelative = useRelativeTimeFormatter();\n * return <span>{formatRelative(new Date())}</span>;\n * ```\n */\nexport function useRelativeTimeFormatter() {\n const formatter = useFormatter();\n return (date: Date | number, options?: Parameters<typeof formatter.relativeTime>[1]) =>\n formatter.relativeTime(date, options);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Locale Switching Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Locales the app declared on `I18nProvider`; the built-in routing list only\n * when it declared none.\n *\n * @example\n * ```tsx\n * const locales = useLocales();\n * // ['en', 'ru', 'ko']\n * ```\n */\nexport function useLocales(): LocaleCode[] {\n const declared = useDeclaredLocales();\n return declared ?? (routing.locales as LocaleCode[]);\n}\n\n/**\n * Get default locale from routing config\n */\nexport function useDefaultLocale(): LocaleCode {\n return routing.defaultLocale as LocaleCode;\n}\n\n/**\n * Hook to change current locale\n *\n * @example\n * ```tsx\n * const changeLocale = useChangeLocale();\n *\n * <button onClick={() => changeLocale('ru')}>\n * Switch to Russian\n * </button>\n * ```\n */\nexport function useChangeLocale() {\n const pathname = usePathname();\n\n return (locale: LocaleCode) => {\n // Set NEXT_LOCALE cookie so middleware remembers the choice\n const date = new Date();\n date.setFullYear(date.getFullYear() + 1);\n document.cookie = `NEXT_LOCALE=${locale}; expires=${date.toUTCString()}; path=/`;\n\n // Hard navigation to force full page reload with new locale\n // (ensures server components re-render with updated translations)\n window.location.href = `/${locale}${pathname}`;\n };\n}\n\n/**\n * Combined hook for locale switching\n * Returns current locale, available locales, and change function\n *\n * @example\n * ```tsx\n * const { locale, locales, changeLocale } = useLocaleSwitcher();\n *\n * <select value={locale} onChange={(e) => changeLocale(e.target.value)}>\n * {locales.map(l => <option key={l} value={l}>{l}</option>)}\n * </select>\n * ```\n */\nexport function useLocaleSwitcher() {\n const locale = useLocale();\n const locales = useLocales();\n const changeLocale = useChangeLocale();\n\n return { locale, locales, changeLocale };\n}\n","/**\n * i18n Navigation Utilities\n *\n * Provides locale-aware navigation components and functions\n *\n * @example\n * ```ts\n * // In your app's i18n/navigation.ts\n * import { createNavigation } from '@djangocfg/nextjs/i18n';\n * import { routing } from './routing';\n *\n * export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);\n * ```\n *\n * @example\n * ```tsx\n * // Using in components\n * import { Link, usePathname } from '@/i18n/navigation';\n *\n * function Nav() {\n * const pathname = usePathname();\n * return <Link href=\"/about\">About</Link>;\n * }\n * ```\n */\n\nimport { createNavigation as createNextIntlNavigation } from 'next-intl/navigation';\n\nimport { createRouting, routing } from './routing';\n\nimport type { I18nConfig } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Navigation Factory\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Create navigation utilities with custom routing\n *\n * Returns locale-aware versions of:\n * - Link: Locale-prefixed links\n * - redirect: Server-side redirect with locale\n * - usePathname: Current pathname without locale prefix\n * - useRouter: Router with locale-aware navigation\n * - getPathname: Get pathname for a route\n */\nexport function createNavigation(routingConfig?: ReturnType<typeof createRouting>) {\n const config = routingConfig ?? routing;\n return createNextIntlNavigation(config);\n}\n\n/**\n * Create navigation from config options\n */\nexport function createNavigationFromConfig(config: Partial<I18nConfig>) {\n const routingConfig = createRouting(config);\n return createNextIntlNavigation(routingConfig);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Default Navigation (using default routing)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Default navigation utilities\n * Use these directly or create custom ones with createNavigation()\n */\nexport const {\n Link,\n redirect,\n usePathname,\n useRouter,\n getPathname,\n} = createNavigation();\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Props for locale-aware Link component\n */\nexport interface LinkProps {\n /** Target href */\n href: string;\n /** Target locale (optional, defaults to current) */\n locale?: string;\n /** Children */\n children?: React.ReactNode;\n /** Additional props passed to Next.js Link */\n [key: string]: unknown;\n}\n","/**\n * i18n Routing Configuration\n *\n * Creates routing configuration for next-intl\n * Used by proxy and navigation components\n */\n\nimport { defineRouting } from 'next-intl/routing';\n\nimport type { I18nConfig, LocaleCode } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Default Configuration\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport const DEFAULT_LOCALES: LocaleCode[] = ['en', 'ru', 'ko', 'ja', 'de', 'fr', 'zh', 'it', 'es', 'nl', 'ar', 'tr', 'pt-BR', 'pl', 'sv', 'no', 'da'];\nexport const DEFAULT_LOCALE: LocaleCode = 'en';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Routing Factory\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Create routing configuration for next-intl\n *\n * @example\n * ```ts\n * // i18n/routing.ts\n * import { createRouting } from '@djangocfg/nextjs/i18n';\n *\n * export const routing = createRouting({\n * locales: ['en', 'ru', 'ko'],\n * defaultLocale: 'en',\n * });\n * ```\n */\nexport function createRouting(config?: Partial<I18nConfig>) {\n const locales = config?.locales ?? DEFAULT_LOCALES;\n const defaultLocale = config?.defaultLocale ?? DEFAULT_LOCALE;\n const localePrefix = config?.localePrefix ?? 'as-needed';\n\n return defineRouting({\n locales,\n defaultLocale,\n localePrefix,\n });\n}\n\n/**\n * Default routing configuration\n * Can be overridden by app-specific configuration\n */\nexport const routing = createRouting();\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Locale Utilities\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Check if a locale is supported\n */\nexport function isValidLocale(\n locale: string,\n supportedLocales: readonly string[] = DEFAULT_LOCALES\n): locale is LocaleCode {\n return supportedLocales.includes(locale as LocaleCode);\n}\n\n/**\n * Get locale from params (handles async params in Next.js 15+)\n */\nexport async function getLocaleFromParams(\n params: Promise<{ locale: string }> | { locale: string }\n): Promise<LocaleCode> {\n const resolved = await params;\n return resolved.locale as LocaleCode;\n}\n\n/**\n * Generate static params for all locales\n * Use in generateStaticParams for locale pages\n */\nexport function generateLocaleParams(locales: readonly string[] = DEFAULT_LOCALES) {\n return locales.map((locale) => ({ locale }));\n}\n","/**\n * i18n Provider for Next.js App Router\n *\n * Bridges next-intl with @djangocfg/i18n\n * Provides translations to both Server and Client components\n *\n * @example\n * ```tsx\n * // app/[locale]/layout.tsx\n * import { I18nProvider } from '@djangocfg/nextjs/i18n';\n * import { getMessages, getLocale } from '@djangocfg/nextjs/i18n/server';\n *\n * export default async function LocaleLayout({ children, params }) {\n * const locale = await getLocale(params);\n * const messages = await getMessages();\n *\n * return (\n * <I18nProvider locale={locale} messages={messages}>\n * {children}\n * </I18nProvider>\n * );\n * }\n * ```\n */\n\n'use client';\n\nimport { createContext, useContext } from 'react';\nimport { NextIntlClientProvider } from 'next-intl';\nimport { I18nProvider as DjangoCfgI18nProvider } from '@djangocfg/i18n';\nimport type { I18nProviderProps, LocaleCode } from './types';\n\nconst DeclaredLocalesContext = createContext<LocaleCode[] | null>(null);\n\n/** Locales the app passed to its provider, or null when it passed none. */\nexport const useDeclaredLocales = (): LocaleCode[] | null => useContext(DeclaredLocalesContext);\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Provider Component\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Unified i18n Provider\n *\n * Wraps both NextIntlClientProvider and @djangocfg/i18n's I18nProvider\n * This ensures both next-intl hooks and @djangocfg/i18n hooks work correctly\n */\nexport function I18nProvider({\n locale,\n messages,\n locales,\n timeZone = 'UTC',\n now,\n children,\n}: I18nProviderProps) {\n return (\n <NextIntlClientProvider\n locale={locale}\n messages={messages}\n timeZone={timeZone}\n now={now}\n >\n <DjangoCfgI18nProvider\n locale={locale as LocaleCode}\n translations={messages}\n >\n <DeclaredLocalesContext.Provider value={locales ?? null}>\n {children}\n </DeclaredLocalesContext.Provider>\n </DjangoCfgI18nProvider>\n </NextIntlClientProvider>\n );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Lightweight Provider (next-intl only)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Next-intl only provider (without @djangocfg/i18n context)\n * Use if you only need next-intl hooks\n */\nexport function NextIntlProvider({\n locale,\n messages,\n locales,\n timeZone = 'UTC',\n now,\n children,\n}: I18nProviderProps) {\n return (\n <NextIntlClientProvider\n locale={locale}\n messages={messages}\n timeZone={timeZone}\n now={now}\n >\n <DeclaredLocalesContext.Provider value={locales ?? null}>\n {children}\n </DeclaredLocalesContext.Provider>\n </NextIntlClientProvider>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/i18n/client.ts","../../src/i18n/navigation.ts","../../src/i18n/routing.ts","../../src/i18n/provider.tsx","../../src/i18n/shared-locale.ts"],"sourcesContent":["/**\n * Client-side i18n Hooks\n *\n * For use in Client Components ('use client')\n *\n * @example\n * ```tsx\n * 'use client';\n *\n * import { useTranslations, useLocale } from '@djangocfg/nextjs/i18n/client';\n *\n * export function MyComponent() {\n * const t = useTranslations('HomePage');\n * const locale = useLocale();\n *\n * return <h1>{t('title')}</h1>;\n * }\n * ```\n */\n\n'use client';\n\nimport {\n useFormatter, useLocale as useNextIntlLocale, useMessages, useNow, useTimeZone,\n useTranslations as useNextIntlTranslations\n} from 'next-intl';\n\nimport { usePathname } from './navigation';\nimport { I18nProvider, NextIntlProvider, useDeclaredLocales } from './provider';\nimport { routing } from './routing';\nimport { expireHostOnlyLocaleCookie, localeCookieHeader, sharedCookieDomain } from './shared-locale';\n\n// Re-export providers\nexport { I18nProvider, NextIntlProvider };\n\nimport type { LocaleCode } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Core Client Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Get translations in Client Components\n *\n * @example\n * ```tsx\n * const t = useTranslations('HomePage');\n * return <h1>{t('title')}</h1>;\n *\n * // With interpolation\n * return <p>{t('greeting', { name: 'John' })}</p>;\n * ```\n */\nexport function useTranslations<Namespace extends string = never>(\n namespace?: Namespace\n) {\n return useNextIntlTranslations(namespace);\n}\n\n/**\n * Get current locale in Client Components\n */\nexport function useLocale(): LocaleCode {\n return useNextIntlLocale() as LocaleCode;\n}\n\n/**\n * Get all messages\n * Useful for passing to child providers\n */\nexport { useMessages };\n\n/**\n * Get current time\n */\nexport { useNow };\n\n/**\n * Get timezone\n */\nexport { useTimeZone };\n\n/**\n * Get formatter for dates, numbers, etc.\n */\nexport { useFormatter };\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Convenience Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Shorthand for useTranslations\n * Alias for compatibility with @djangocfg/i18n\n */\nexport const useT = useTranslations;\n\n/**\n * Get namespaced translations\n *\n * @example\n * ```tsx\n * const pt = useNamespacedTranslations('payments');\n * return <span>{pt('balance.available')}</span>;\n * ```\n */\nexport function useNamespacedTranslations(namespace: string) {\n return useNextIntlTranslations(namespace);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Formatting Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Format a date according to locale\n *\n * @example\n * ```tsx\n * const formatDate = useDateFormatter();\n * return <span>{formatDate(new Date())}</span>;\n * ```\n */\nexport function useDateFormatter() {\n const formatter = useFormatter();\n return (date: Date | number, options?: Parameters<typeof formatter.dateTime>[1]) =>\n formatter.dateTime(date, options);\n}\n\n/**\n * Format a number according to locale\n *\n * @example\n * ```tsx\n * const formatNumber = useNumberFormatter();\n * return <span>{formatNumber(1234.56)}</span>;\n * ```\n */\nexport function useNumberFormatter() {\n const formatter = useFormatter();\n return (number: number, options?: Parameters<typeof formatter.number>[1]) =>\n formatter.number(number, options);\n}\n\n/**\n * Format relative time\n *\n * @example\n * ```tsx\n * const formatRelative = useRelativeTimeFormatter();\n * return <span>{formatRelative(new Date())}</span>;\n * ```\n */\nexport function useRelativeTimeFormatter() {\n const formatter = useFormatter();\n return (date: Date | number, options?: Parameters<typeof formatter.relativeTime>[1]) =>\n formatter.relativeTime(date, options);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Locale Switching Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Locales the app declared on `I18nProvider`; the built-in routing list only\n * when it declared none.\n *\n * @example\n * ```tsx\n * const locales = useLocales();\n * // ['en', 'ru', 'ko']\n * ```\n */\nexport function useLocales(): LocaleCode[] {\n const declared = useDeclaredLocales();\n return declared ?? (routing.locales as LocaleCode[]);\n}\n\n/**\n * Get default locale from routing config\n */\nexport function useDefaultLocale(): LocaleCode {\n return routing.defaultLocale as LocaleCode;\n}\n\n/**\n * Hook to change current locale\n *\n * @example\n * ```tsx\n * const changeLocale = useChangeLocale();\n *\n * <button onClick={() => changeLocale('ru')}>\n * Switch to Russian\n * </button>\n * ```\n */\nexport function useChangeLocale() {\n const pathname = usePathname();\n\n return (locale: LocaleCode) => {\n // Set NEXT_LOCALE so middleware remembers the choice, on the registrable domain so every host of the family\n // reads it (see ./shared-locale); the host-only twin is expired so it cannot shadow the shared one.\n const secure = window.location.protocol === 'https:';\n const domain = sharedCookieDomain(window.location.hostname, process.env.NEXT_PUBLIC_COOKIE_DOMAIN);\n document.cookie = localeCookieHeader(locale, domain, secure);\n if (domain) document.cookie = expireHostOnlyLocaleCookie(secure);\n\n // Hard navigation to force full page reload with new locale\n // (ensures server components re-render with updated translations)\n window.location.href = `/${locale}${pathname}`;\n };\n}\n\n/**\n * Combined hook for locale switching\n * Returns current locale, available locales, and change function\n *\n * @example\n * ```tsx\n * const { locale, locales, changeLocale } = useLocaleSwitcher();\n *\n * <select value={locale} onChange={(e) => changeLocale(e.target.value)}>\n * {locales.map(l => <option key={l} value={l}>{l}</option>)}\n * </select>\n * ```\n */\nexport function useLocaleSwitcher() {\n const locale = useLocale();\n const locales = useLocales();\n const changeLocale = useChangeLocale();\n\n return { locale, locales, changeLocale };\n}\n","/**\n * i18n Navigation Utilities\n *\n * Provides locale-aware navigation components and functions\n *\n * @example\n * ```ts\n * // In your app's i18n/navigation.ts\n * import { createNavigation } from '@djangocfg/nextjs/i18n';\n * import { routing } from './routing';\n *\n * export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);\n * ```\n *\n * @example\n * ```tsx\n * // Using in components\n * import { Link, usePathname } from '@/i18n/navigation';\n *\n * function Nav() {\n * const pathname = usePathname();\n * return <Link href=\"/about\">About</Link>;\n * }\n * ```\n */\n\nimport { createNavigation as createNextIntlNavigation } from 'next-intl/navigation';\n\nimport { createRouting, routing } from './routing';\n\nimport type { I18nConfig } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Navigation Factory\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Create navigation utilities with custom routing\n *\n * Returns locale-aware versions of:\n * - Link: Locale-prefixed links\n * - redirect: Server-side redirect with locale\n * - usePathname: Current pathname without locale prefix\n * - useRouter: Router with locale-aware navigation\n * - getPathname: Get pathname for a route\n */\nexport function createNavigation(routingConfig?: ReturnType<typeof createRouting>) {\n const config = routingConfig ?? routing;\n return createNextIntlNavigation(config);\n}\n\n/**\n * Create navigation from config options\n */\nexport function createNavigationFromConfig(config: Partial<I18nConfig>) {\n const routingConfig = createRouting(config);\n return createNextIntlNavigation(routingConfig);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Default Navigation (using default routing)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Default navigation utilities\n * Use these directly or create custom ones with createNavigation()\n */\nexport const {\n Link,\n redirect,\n usePathname,\n useRouter,\n getPathname,\n} = createNavigation();\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Props for locale-aware Link component\n */\nexport interface LinkProps {\n /** Target href */\n href: string;\n /** Target locale (optional, defaults to current) */\n locale?: string;\n /** Children */\n children?: React.ReactNode;\n /** Additional props passed to Next.js Link */\n [key: string]: unknown;\n}\n","/**\n * i18n Routing Configuration\n *\n * Creates routing configuration for next-intl\n * Used by proxy and navigation components\n */\n\nimport { defineRouting } from 'next-intl/routing';\n\nimport type { I18nConfig, LocaleCode } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Default Configuration\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport const DEFAULT_LOCALES: LocaleCode[] = ['en', 'ru', 'ko', 'ja', 'de', 'fr', 'zh', 'it', 'es', 'nl', 'ar', 'tr', 'pt-BR', 'pl', 'sv', 'no', 'da'];\nexport const DEFAULT_LOCALE: LocaleCode = 'en';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Routing Factory\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Create routing configuration for next-intl\n *\n * @example\n * ```ts\n * // i18n/routing.ts\n * import { createRouting } from '@djangocfg/nextjs/i18n';\n *\n * export const routing = createRouting({\n * locales: ['en', 'ru', 'ko'],\n * defaultLocale: 'en',\n * });\n * ```\n */\nexport function createRouting(config?: Partial<I18nConfig>) {\n const locales = config?.locales ?? DEFAULT_LOCALES;\n const defaultLocale = config?.defaultLocale ?? DEFAULT_LOCALE;\n const localePrefix = config?.localePrefix ?? 'as-needed';\n\n return defineRouting({\n locales,\n defaultLocale,\n localePrefix,\n });\n}\n\n/**\n * Default routing configuration\n * Can be overridden by app-specific configuration\n */\nexport const routing = createRouting();\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Locale Utilities\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Check if a locale is supported\n */\nexport function isValidLocale(\n locale: string,\n supportedLocales: readonly string[] = DEFAULT_LOCALES\n): locale is LocaleCode {\n return supportedLocales.includes(locale as LocaleCode);\n}\n\n/**\n * Get locale from params (handles async params in Next.js 15+)\n */\nexport async function getLocaleFromParams(\n params: Promise<{ locale: string }> | { locale: string }\n): Promise<LocaleCode> {\n const resolved = await params;\n return resolved.locale as LocaleCode;\n}\n\n/**\n * Generate static params for all locales\n * Use in generateStaticParams for locale pages\n */\nexport function generateLocaleParams(locales: readonly string[] = DEFAULT_LOCALES) {\n return locales.map((locale) => ({ locale }));\n}\n","/**\n * i18n Provider for Next.js App Router\n *\n * Bridges next-intl with @djangocfg/i18n\n * Provides translations to both Server and Client components\n *\n * @example\n * ```tsx\n * // app/[locale]/layout.tsx\n * import { I18nProvider } from '@djangocfg/nextjs/i18n';\n * import { getMessages, getLocale } from '@djangocfg/nextjs/i18n/server';\n *\n * export default async function LocaleLayout({ children, params }) {\n * const locale = await getLocale(params);\n * const messages = await getMessages();\n *\n * return (\n * <I18nProvider locale={locale} messages={messages}>\n * {children}\n * </I18nProvider>\n * );\n * }\n * ```\n */\n\n'use client';\n\nimport { createContext, useContext } from 'react';\nimport { NextIntlClientProvider } from 'next-intl';\nimport { I18nProvider as DjangoCfgI18nProvider } from '@djangocfg/i18n';\nimport type { I18nProviderProps, LocaleCode } from './types';\n\nconst DeclaredLocalesContext = createContext<LocaleCode[] | null>(null);\n\n/** Locales the app passed to its provider, or null when it passed none. */\nexport const useDeclaredLocales = (): LocaleCode[] | null => useContext(DeclaredLocalesContext);\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Provider Component\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Unified i18n Provider\n *\n * Wraps both NextIntlClientProvider and @djangocfg/i18n's I18nProvider\n * This ensures both next-intl hooks and @djangocfg/i18n hooks work correctly\n */\nexport function I18nProvider({\n locale,\n messages,\n locales,\n timeZone = 'UTC',\n now,\n children,\n}: I18nProviderProps) {\n return (\n <NextIntlClientProvider\n locale={locale}\n messages={messages}\n timeZone={timeZone}\n now={now}\n >\n <DjangoCfgI18nProvider\n locale={locale as LocaleCode}\n translations={messages}\n >\n <DeclaredLocalesContext.Provider value={locales ?? null}>\n {children}\n </DeclaredLocalesContext.Provider>\n </DjangoCfgI18nProvider>\n </NextIntlClientProvider>\n );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Lightweight Provider (next-intl only)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Next-intl only provider (without @djangocfg/i18n context)\n * Use if you only need next-intl hooks\n */\nexport function NextIntlProvider({\n locale,\n messages,\n locales,\n timeZone = 'UTC',\n now,\n children,\n}: I18nProviderProps) {\n return (\n <NextIntlClientProvider\n locale={locale}\n messages={messages}\n timeZone={timeZone}\n now={now}\n >\n <DeclaredLocalesContext.Provider value={locales ?? null}>\n {children}\n </DeclaredLocalesContext.Provider>\n </NextIntlClientProvider>\n );\n}\n","/**\n * One locale across every host of one product family.\n *\n * `NEXT_LOCALE` is written host-only by default, so a reader who picked Russian on `hub.example.com` meets English on\n * `team.example.com`. The session tokens already live on the registrable domain (`@djangocfg/api` `createStorage`,\n * `cookieDomain: 'auto'`); the locale follows the same rule: the cookie gets `Domain=` the registrable domain, and the\n * host-only twin is expired so it cannot shadow the shared one.\n *\n * Pure and framework-free, so the proxy (server) and `useChangeLocale` (browser) agree on the domain.\n */\n\nexport const LOCALE_COOKIE = 'NEXT_LOCALE';\nexport const LOCALE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;\n\n// Second-level public suffixes whose registrable domain has three labels. Not the whole Public Suffix List: the common\n// ones; anything else sets NEXT_PUBLIC_COOKIE_DOMAIN.\nconst TWO_LABEL_SUFFIXES = new Set([\n 'co.uk', 'org.uk', 'ac.uk', 'com.au', 'net.au', 'co.nz', 'co.jp', 'co.kr', 'or.kr', 'com.br', 'com.tr', 'com.cn', 'com.mx',\n 'co.in', 'co.za', 'com.sg', 'com.hk', 'com.tw', 'com.ar', 'com.ua', 'co.il',\n]);\n\n/**\n * The domain a cookie must carry to be sent to every subdomain, or `null` for a host that cannot share (an IP, a bare\n * label such as `localhost`, or an apex with no subdomain in use: a host-only cookie already covers it).\n * `override` (`NEXT_PUBLIC_COOKIE_DOMAIN`) wins when set.\n */\nexport function sharedCookieDomain(hostname: string, override?: string | null): string | null {\n const forced = override?.trim().replace(/^\\./, '');\n if (forced) return forced;\n const host = hostname.split(':')[0]!.toLowerCase();\n if (!host || /^\\d+(\\.\\d+){3}$/.test(host) || host.includes(':')) return null;\n const labels = host.split('.');\n if (labels.length < 2) return null;\n const keep = TWO_LABEL_SUFFIXES.has(labels.slice(-2).join('.')) ? 3 : 2;\n return labels.slice(-keep).join('.');\n}\n\n/** The `Set-Cookie` value that writes the shared locale cookie. */\nexport function localeCookieHeader(locale: string, domain: string | null, secure: boolean): string {\n return `${LOCALE_COOKIE}=${encodeURIComponent(locale)}; Path=/; Max-Age=${LOCALE_COOKIE_MAX_AGE}; SameSite=Lax${domain ? `; Domain=${domain}` : ''}${secure ? '; Secure' : ''}`;\n}\n\n/** Expires the host-only twin of the cookie (a cookie is identified by name and Domain). */\nexport function expireHostOnlyLocaleCookie(secure: boolean): string {\n return `${LOCALE_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax${secure ? '; Secure' : ''}`;\n}\n"],"mappings":"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","names":[]}
@@ -54,6 +54,49 @@ import { jsx } from "react/jsx-runtime";
54
54
  var DeclaredLocalesContext = createContext(null);
55
55
  var useDeclaredLocales = () => useContext(DeclaredLocalesContext);
56
56
 
57
+ // src/i18n/shared-locale.ts
58
+ var LOCALE_COOKIE = "NEXT_LOCALE";
59
+ var LOCALE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
60
+ var TWO_LABEL_SUFFIXES = /* @__PURE__ */ new Set([
61
+ "co.uk",
62
+ "org.uk",
63
+ "ac.uk",
64
+ "com.au",
65
+ "net.au",
66
+ "co.nz",
67
+ "co.jp",
68
+ "co.kr",
69
+ "or.kr",
70
+ "com.br",
71
+ "com.tr",
72
+ "com.cn",
73
+ "com.mx",
74
+ "co.in",
75
+ "co.za",
76
+ "com.sg",
77
+ "com.hk",
78
+ "com.tw",
79
+ "com.ar",
80
+ "com.ua",
81
+ "co.il"
82
+ ]);
83
+ function sharedCookieDomain(hostname, override) {
84
+ const forced = override?.trim().replace(/^\./, "");
85
+ if (forced) return forced;
86
+ const host = hostname.split(":")[0].toLowerCase();
87
+ if (!host || /^\d+(\.\d+){3}$/.test(host) || host.includes(":")) return null;
88
+ const labels = host.split(".");
89
+ if (labels.length < 2) return null;
90
+ const keep = TWO_LABEL_SUFFIXES.has(labels.slice(-2).join(".")) ? 3 : 2;
91
+ return labels.slice(-keep).join(".");
92
+ }
93
+ function localeCookieHeader(locale, domain, secure) {
94
+ return `${LOCALE_COOKIE}=${encodeURIComponent(locale)}; Path=/; Max-Age=${LOCALE_COOKIE_MAX_AGE}; SameSite=Lax${domain ? `; Domain=${domain}` : ""}${secure ? "; Secure" : ""}`;
95
+ }
96
+ function expireHostOnlyLocaleCookie(secure) {
97
+ return `${LOCALE_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax${secure ? "; Secure" : ""}`;
98
+ }
99
+
57
100
  // src/i18n/client.ts
58
101
  function useLocale() {
59
102
  return useNextIntlLocale();
@@ -65,9 +108,10 @@ function useLocales() {
65
108
  function useChangeLocale() {
66
109
  const pathname = usePathname();
67
110
  return (locale) => {
68
- const date = /* @__PURE__ */ new Date();
69
- date.setFullYear(date.getFullYear() + 1);
70
- document.cookie = `NEXT_LOCALE=${locale}; expires=${date.toUTCString()}; path=/`;
111
+ const secure = window.location.protocol === "https:";
112
+ const domain = sharedCookieDomain(window.location.hostname, process.env.NEXT_PUBLIC_COOKIE_DOMAIN);
113
+ document.cookie = localeCookieHeader(locale, domain, secure);
114
+ if (domain) document.cookie = expireHostOnlyLocaleCookie(secure);
71
115
  window.location.href = `/${locale}${pathname}`;
72
116
  };
73
117
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/i18n/components/LocaleSwitcher.tsx","../../src/i18n/client.ts","../../src/i18n/navigation.ts","../../src/i18n/routing.ts","../../src/i18n/provider.tsx"],"sourcesContent":["/**\n * LocaleSwitcher Component (Smart)\n *\n * Wrapper around `@djangocfg/layouts` LocaleSwitcher with next-intl plumbing.\n * Mounts its own `LayoutI18nProvider` so the switcher works standalone — no\n * need to wrap the page in `<AppLayout i18n={...}>` just to render it.\n *\n * @example\n * ```tsx\n * import { LocaleSwitcher } from '@djangocfg/nextjs/i18n/components';\n *\n * <LocaleSwitcher />\n * <LocaleSwitcher locales={['en', 'ru']} />\n * <LocaleSwitcher labels={{ en: 'English', ru: 'Русский' }} />\n * ```\n */\n\n'use client';\n\nimport {\n LayoutI18nProvider,\n LocaleSwitcher as BaseLocaleSwitcher,\n type LocaleSwitcherProps as BaseLocaleSwitcherProps,\n} from '@djangocfg/layouts';\n\nimport { useLocaleSwitcher } from '../client';\nimport type { LocaleCode } from '../types';\n\nexport interface LocaleSwitcherProps extends BaseLocaleSwitcherProps {\n /** Available locales (defaults to all from routing config) */\n locales?: LocaleCode[];\n}\n\nexport function LocaleSwitcher({ locales: customLocales, ...props }: LocaleSwitcherProps) {\n const { locale, locales: routingLocales, changeLocale } = useLocaleSwitcher();\n\n const availableLocales = customLocales || routingLocales;\n\n return (\n <LayoutI18nProvider\n value={{\n locale,\n locales: availableLocales,\n onLocaleChange: changeLocale,\n }}\n >\n <BaseLocaleSwitcher {...props} />\n </LayoutI18nProvider>\n );\n}\n","/**\n * Client-side i18n Hooks\n *\n * For use in Client Components ('use client')\n *\n * @example\n * ```tsx\n * 'use client';\n *\n * import { useTranslations, useLocale } from '@djangocfg/nextjs/i18n/client';\n *\n * export function MyComponent() {\n * const t = useTranslations('HomePage');\n * const locale = useLocale();\n *\n * return <h1>{t('title')}</h1>;\n * }\n * ```\n */\n\n'use client';\n\nimport {\n useFormatter, useLocale as useNextIntlLocale, useMessages, useNow, useTimeZone,\n useTranslations as useNextIntlTranslations\n} from 'next-intl';\n\nimport { usePathname } from './navigation';\nimport { I18nProvider, NextIntlProvider, useDeclaredLocales } from './provider';\nimport { routing } from './routing';\n\n// Re-export providers\nexport { I18nProvider, NextIntlProvider };\n\nimport type { LocaleCode } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Core Client Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Get translations in Client Components\n *\n * @example\n * ```tsx\n * const t = useTranslations('HomePage');\n * return <h1>{t('title')}</h1>;\n *\n * // With interpolation\n * return <p>{t('greeting', { name: 'John' })}</p>;\n * ```\n */\nexport function useTranslations<Namespace extends string = never>(\n namespace?: Namespace\n) {\n return useNextIntlTranslations(namespace);\n}\n\n/**\n * Get current locale in Client Components\n */\nexport function useLocale(): LocaleCode {\n return useNextIntlLocale() as LocaleCode;\n}\n\n/**\n * Get all messages\n * Useful for passing to child providers\n */\nexport { useMessages };\n\n/**\n * Get current time\n */\nexport { useNow };\n\n/**\n * Get timezone\n */\nexport { useTimeZone };\n\n/**\n * Get formatter for dates, numbers, etc.\n */\nexport { useFormatter };\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Convenience Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Shorthand for useTranslations\n * Alias for compatibility with @djangocfg/i18n\n */\nexport const useT = useTranslations;\n\n/**\n * Get namespaced translations\n *\n * @example\n * ```tsx\n * const pt = useNamespacedTranslations('payments');\n * return <span>{pt('balance.available')}</span>;\n * ```\n */\nexport function useNamespacedTranslations(namespace: string) {\n return useNextIntlTranslations(namespace);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Formatting Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Format a date according to locale\n *\n * @example\n * ```tsx\n * const formatDate = useDateFormatter();\n * return <span>{formatDate(new Date())}</span>;\n * ```\n */\nexport function useDateFormatter() {\n const formatter = useFormatter();\n return (date: Date | number, options?: Parameters<typeof formatter.dateTime>[1]) =>\n formatter.dateTime(date, options);\n}\n\n/**\n * Format a number according to locale\n *\n * @example\n * ```tsx\n * const formatNumber = useNumberFormatter();\n * return <span>{formatNumber(1234.56)}</span>;\n * ```\n */\nexport function useNumberFormatter() {\n const formatter = useFormatter();\n return (number: number, options?: Parameters<typeof formatter.number>[1]) =>\n formatter.number(number, options);\n}\n\n/**\n * Format relative time\n *\n * @example\n * ```tsx\n * const formatRelative = useRelativeTimeFormatter();\n * return <span>{formatRelative(new Date())}</span>;\n * ```\n */\nexport function useRelativeTimeFormatter() {\n const formatter = useFormatter();\n return (date: Date | number, options?: Parameters<typeof formatter.relativeTime>[1]) =>\n formatter.relativeTime(date, options);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Locale Switching Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Locales the app declared on `I18nProvider`; the built-in routing list only\n * when it declared none.\n *\n * @example\n * ```tsx\n * const locales = useLocales();\n * // ['en', 'ru', 'ko']\n * ```\n */\nexport function useLocales(): LocaleCode[] {\n const declared = useDeclaredLocales();\n return declared ?? (routing.locales as LocaleCode[]);\n}\n\n/**\n * Get default locale from routing config\n */\nexport function useDefaultLocale(): LocaleCode {\n return routing.defaultLocale as LocaleCode;\n}\n\n/**\n * Hook to change current locale\n *\n * @example\n * ```tsx\n * const changeLocale = useChangeLocale();\n *\n * <button onClick={() => changeLocale('ru')}>\n * Switch to Russian\n * </button>\n * ```\n */\nexport function useChangeLocale() {\n const pathname = usePathname();\n\n return (locale: LocaleCode) => {\n // Set NEXT_LOCALE cookie so middleware remembers the choice\n const date = new Date();\n date.setFullYear(date.getFullYear() + 1);\n document.cookie = `NEXT_LOCALE=${locale}; expires=${date.toUTCString()}; path=/`;\n\n // Hard navigation to force full page reload with new locale\n // (ensures server components re-render with updated translations)\n window.location.href = `/${locale}${pathname}`;\n };\n}\n\n/**\n * Combined hook for locale switching\n * Returns current locale, available locales, and change function\n *\n * @example\n * ```tsx\n * const { locale, locales, changeLocale } = useLocaleSwitcher();\n *\n * <select value={locale} onChange={(e) => changeLocale(e.target.value)}>\n * {locales.map(l => <option key={l} value={l}>{l}</option>)}\n * </select>\n * ```\n */\nexport function useLocaleSwitcher() {\n const locale = useLocale();\n const locales = useLocales();\n const changeLocale = useChangeLocale();\n\n return { locale, locales, changeLocale };\n}\n","/**\n * i18n Navigation Utilities\n *\n * Provides locale-aware navigation components and functions\n *\n * @example\n * ```ts\n * // In your app's i18n/navigation.ts\n * import { createNavigation } from '@djangocfg/nextjs/i18n';\n * import { routing } from './routing';\n *\n * export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);\n * ```\n *\n * @example\n * ```tsx\n * // Using in components\n * import { Link, usePathname } from '@/i18n/navigation';\n *\n * function Nav() {\n * const pathname = usePathname();\n * return <Link href=\"/about\">About</Link>;\n * }\n * ```\n */\n\nimport { createNavigation as createNextIntlNavigation } from 'next-intl/navigation';\n\nimport { createRouting, routing } from './routing';\n\nimport type { I18nConfig } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Navigation Factory\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Create navigation utilities with custom routing\n *\n * Returns locale-aware versions of:\n * - Link: Locale-prefixed links\n * - redirect: Server-side redirect with locale\n * - usePathname: Current pathname without locale prefix\n * - useRouter: Router with locale-aware navigation\n * - getPathname: Get pathname for a route\n */\nexport function createNavigation(routingConfig?: ReturnType<typeof createRouting>) {\n const config = routingConfig ?? routing;\n return createNextIntlNavigation(config);\n}\n\n/**\n * Create navigation from config options\n */\nexport function createNavigationFromConfig(config: Partial<I18nConfig>) {\n const routingConfig = createRouting(config);\n return createNextIntlNavigation(routingConfig);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Default Navigation (using default routing)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Default navigation utilities\n * Use these directly or create custom ones with createNavigation()\n */\nexport const {\n Link,\n redirect,\n usePathname,\n useRouter,\n getPathname,\n} = createNavigation();\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Props for locale-aware Link component\n */\nexport interface LinkProps {\n /** Target href */\n href: string;\n /** Target locale (optional, defaults to current) */\n locale?: string;\n /** Children */\n children?: React.ReactNode;\n /** Additional props passed to Next.js Link */\n [key: string]: unknown;\n}\n","/**\n * i18n Routing Configuration\n *\n * Creates routing configuration for next-intl\n * Used by proxy and navigation components\n */\n\nimport { defineRouting } from 'next-intl/routing';\n\nimport type { I18nConfig, LocaleCode } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Default Configuration\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport const DEFAULT_LOCALES: LocaleCode[] = ['en', 'ru', 'ko', 'ja', 'de', 'fr', 'zh', 'it', 'es', 'nl', 'ar', 'tr', 'pt-BR', 'pl', 'sv', 'no', 'da'];\nexport const DEFAULT_LOCALE: LocaleCode = 'en';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Routing Factory\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Create routing configuration for next-intl\n *\n * @example\n * ```ts\n * // i18n/routing.ts\n * import { createRouting } from '@djangocfg/nextjs/i18n';\n *\n * export const routing = createRouting({\n * locales: ['en', 'ru', 'ko'],\n * defaultLocale: 'en',\n * });\n * ```\n */\nexport function createRouting(config?: Partial<I18nConfig>) {\n const locales = config?.locales ?? DEFAULT_LOCALES;\n const defaultLocale = config?.defaultLocale ?? DEFAULT_LOCALE;\n const localePrefix = config?.localePrefix ?? 'as-needed';\n\n return defineRouting({\n locales,\n defaultLocale,\n localePrefix,\n });\n}\n\n/**\n * Default routing configuration\n * Can be overridden by app-specific configuration\n */\nexport const routing = createRouting();\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Locale Utilities\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Check if a locale is supported\n */\nexport function isValidLocale(\n locale: string,\n supportedLocales: readonly string[] = DEFAULT_LOCALES\n): locale is LocaleCode {\n return supportedLocales.includes(locale as LocaleCode);\n}\n\n/**\n * Get locale from params (handles async params in Next.js 15+)\n */\nexport async function getLocaleFromParams(\n params: Promise<{ locale: string }> | { locale: string }\n): Promise<LocaleCode> {\n const resolved = await params;\n return resolved.locale as LocaleCode;\n}\n\n/**\n * Generate static params for all locales\n * Use in generateStaticParams for locale pages\n */\nexport function generateLocaleParams(locales: readonly string[] = DEFAULT_LOCALES) {\n return locales.map((locale) => ({ locale }));\n}\n","/**\n * i18n Provider for Next.js App Router\n *\n * Bridges next-intl with @djangocfg/i18n\n * Provides translations to both Server and Client components\n *\n * @example\n * ```tsx\n * // app/[locale]/layout.tsx\n * import { I18nProvider } from '@djangocfg/nextjs/i18n';\n * import { getMessages, getLocale } from '@djangocfg/nextjs/i18n/server';\n *\n * export default async function LocaleLayout({ children, params }) {\n * const locale = await getLocale(params);\n * const messages = await getMessages();\n *\n * return (\n * <I18nProvider locale={locale} messages={messages}>\n * {children}\n * </I18nProvider>\n * );\n * }\n * ```\n */\n\n'use client';\n\nimport { createContext, useContext } from 'react';\nimport { NextIntlClientProvider } from 'next-intl';\nimport { I18nProvider as DjangoCfgI18nProvider } from '@djangocfg/i18n';\nimport type { I18nProviderProps, LocaleCode } from './types';\n\nconst DeclaredLocalesContext = createContext<LocaleCode[] | null>(null);\n\n/** Locales the app passed to its provider, or null when it passed none. */\nexport const useDeclaredLocales = (): LocaleCode[] | null => useContext(DeclaredLocalesContext);\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Provider Component\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Unified i18n Provider\n *\n * Wraps both NextIntlClientProvider and @djangocfg/i18n's I18nProvider\n * This ensures both next-intl hooks and @djangocfg/i18n hooks work correctly\n */\nexport function I18nProvider({\n locale,\n messages,\n locales,\n timeZone = 'UTC',\n now,\n children,\n}: I18nProviderProps) {\n return (\n <NextIntlClientProvider\n locale={locale}\n messages={messages}\n timeZone={timeZone}\n now={now}\n >\n <DjangoCfgI18nProvider\n locale={locale as LocaleCode}\n translations={messages}\n >\n <DeclaredLocalesContext.Provider value={locales ?? null}>\n {children}\n </DeclaredLocalesContext.Provider>\n </DjangoCfgI18nProvider>\n </NextIntlClientProvider>\n );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Lightweight Provider (next-intl only)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Next-intl only provider (without @djangocfg/i18n context)\n * Use if you only need next-intl hooks\n */\nexport function NextIntlProvider({\n locale,\n messages,\n locales,\n timeZone = 'UTC',\n now,\n children,\n}: I18nProviderProps) {\n return (\n <NextIntlClientProvider\n locale={locale}\n messages={messages}\n timeZone={timeZone}\n now={now}\n >\n <DeclaredLocalesContext.Provider value={locales ?? null}>\n {children}\n </DeclaredLocalesContext.Provider>\n </NextIntlClientProvider>\n );\n}\n"],"mappings":";AAmBA;AAAA,EACE;AAAA,EACA,kBAAkB;AAAA,OAEb;;;ACDP;AAAA,EACI;AAAA,EAAc,aAAa;AAAA,EAAmB;AAAA,EAAa;AAAA,EAAQ;AAAA,EACnE,mBAAmB;AAAA,OAChB;;;ACCP,SAAS,oBAAoB,gCAAgC;;;ACnB7D,SAAS,qBAAqB;AAQvB,IAAM,kBAAgC,CAAC,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,SAAS,MAAM,MAAM,MAAM,IAAI;AAC9I,IAAM,iBAA6B;AAoBnC,SAAS,cAAc,QAA8B;AAC1D,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,gBAAgB,QAAQ,iBAAiB;AAC/C,QAAM,eAAe,QAAQ,gBAAgB;AAE7C,SAAO,cAAc;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAMO,IAAM,UAAU,cAAc;;;ADN9B,SAAS,iBAAiB,eAAkD;AACjF,QAAM,SAAS,iBAAiB;AAChC,SAAO,yBAAyB,MAAM;AACxC;AAkBO,IAAM;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,IAAI,iBAAiB;;;AE9CrB,SAAS,eAAe,kBAAkB;AAC1C,SAAS,8BAA8B;AACvC,SAAS,gBAAgB,6BAA6B;AAqC9C;AAlCR,IAAM,yBAAyB,cAAmC,IAAI;AAG/D,IAAM,qBAAqB,MAA2B,WAAW,sBAAsB;;;AH0BvF,SAAS,YAAwB;AACtC,SAAO,kBAAkB;AAC3B;AA6GO,SAAS,aAA2B;AACzC,QAAM,WAAW,mBAAmB;AACpC,SAAO,YAAa,QAAQ;AAC9B;AAqBO,SAAS,kBAAkB;AAChC,QAAM,WAAW,YAAY;AAE7B,SAAO,CAAC,WAAuB;AAE7B,UAAM,OAAO,oBAAI,KAAK;AACtB,SAAK,YAAY,KAAK,YAAY,IAAI,CAAC;AACvC,aAAS,SAAS,eAAe,MAAM,aAAa,KAAK,YAAY,CAAC;AAItE,WAAO,SAAS,OAAO,IAAI,MAAM,GAAG,QAAQ;AAAA,EAC9C;AACF;AAeO,SAAS,oBAAoB;AAClC,QAAM,SAAS,UAAU;AACzB,QAAM,UAAU,WAAW;AAC3B,QAAM,eAAe,gBAAgB;AAErC,SAAO,EAAE,QAAQ,SAAS,aAAa;AACzC;;;ADxLM,gBAAAA,YAAA;AAbC,SAAS,eAAe,EAAE,SAAS,eAAe,GAAG,MAAM,GAAwB;AACxF,QAAM,EAAE,QAAQ,SAAS,gBAAgB,aAAa,IAAI,kBAAkB;AAE5E,QAAM,mBAAmB,iBAAiB;AAE1C,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,QACL;AAAA,QACA,SAAS;AAAA,QACT,gBAAgB;AAAA,MAClB;AAAA,MAEA,0BAAAA,KAAC,sBAAoB,GAAG,OAAO;AAAA;AAAA,EACjC;AAEJ;","names":["jsx"]}
1
+ {"version":3,"sources":["../../src/i18n/components/LocaleSwitcher.tsx","../../src/i18n/client.ts","../../src/i18n/navigation.ts","../../src/i18n/routing.ts","../../src/i18n/provider.tsx","../../src/i18n/shared-locale.ts"],"sourcesContent":["/**\n * LocaleSwitcher Component (Smart)\n *\n * Wrapper around `@djangocfg/layouts` LocaleSwitcher with next-intl plumbing.\n * Mounts its own `LayoutI18nProvider` so the switcher works standalone — no\n * need to wrap the page in `<AppLayout i18n={...}>` just to render it.\n *\n * @example\n * ```tsx\n * import { LocaleSwitcher } from '@djangocfg/nextjs/i18n/components';\n *\n * <LocaleSwitcher />\n * <LocaleSwitcher locales={['en', 'ru']} />\n * <LocaleSwitcher labels={{ en: 'English', ru: 'Русский' }} />\n * ```\n */\n\n'use client';\n\nimport {\n LayoutI18nProvider,\n LocaleSwitcher as BaseLocaleSwitcher,\n type LocaleSwitcherProps as BaseLocaleSwitcherProps,\n} from '@djangocfg/layouts';\n\nimport { useLocaleSwitcher } from '../client';\nimport type { LocaleCode } from '../types';\n\nexport interface LocaleSwitcherProps extends BaseLocaleSwitcherProps {\n /** Available locales (defaults to all from routing config) */\n locales?: LocaleCode[];\n}\n\nexport function LocaleSwitcher({ locales: customLocales, ...props }: LocaleSwitcherProps) {\n const { locale, locales: routingLocales, changeLocale } = useLocaleSwitcher();\n\n const availableLocales = customLocales || routingLocales;\n\n return (\n <LayoutI18nProvider\n value={{\n locale,\n locales: availableLocales,\n onLocaleChange: changeLocale,\n }}\n >\n <BaseLocaleSwitcher {...props} />\n </LayoutI18nProvider>\n );\n}\n","/**\n * Client-side i18n Hooks\n *\n * For use in Client Components ('use client')\n *\n * @example\n * ```tsx\n * 'use client';\n *\n * import { useTranslations, useLocale } from '@djangocfg/nextjs/i18n/client';\n *\n * export function MyComponent() {\n * const t = useTranslations('HomePage');\n * const locale = useLocale();\n *\n * return <h1>{t('title')}</h1>;\n * }\n * ```\n */\n\n'use client';\n\nimport {\n useFormatter, useLocale as useNextIntlLocale, useMessages, useNow, useTimeZone,\n useTranslations as useNextIntlTranslations\n} from 'next-intl';\n\nimport { usePathname } from './navigation';\nimport { I18nProvider, NextIntlProvider, useDeclaredLocales } from './provider';\nimport { routing } from './routing';\nimport { expireHostOnlyLocaleCookie, localeCookieHeader, sharedCookieDomain } from './shared-locale';\n\n// Re-export providers\nexport { I18nProvider, NextIntlProvider };\n\nimport type { LocaleCode } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Core Client Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Get translations in Client Components\n *\n * @example\n * ```tsx\n * const t = useTranslations('HomePage');\n * return <h1>{t('title')}</h1>;\n *\n * // With interpolation\n * return <p>{t('greeting', { name: 'John' })}</p>;\n * ```\n */\nexport function useTranslations<Namespace extends string = never>(\n namespace?: Namespace\n) {\n return useNextIntlTranslations(namespace);\n}\n\n/**\n * Get current locale in Client Components\n */\nexport function useLocale(): LocaleCode {\n return useNextIntlLocale() as LocaleCode;\n}\n\n/**\n * Get all messages\n * Useful for passing to child providers\n */\nexport { useMessages };\n\n/**\n * Get current time\n */\nexport { useNow };\n\n/**\n * Get timezone\n */\nexport { useTimeZone };\n\n/**\n * Get formatter for dates, numbers, etc.\n */\nexport { useFormatter };\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Convenience Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Shorthand for useTranslations\n * Alias for compatibility with @djangocfg/i18n\n */\nexport const useT = useTranslations;\n\n/**\n * Get namespaced translations\n *\n * @example\n * ```tsx\n * const pt = useNamespacedTranslations('payments');\n * return <span>{pt('balance.available')}</span>;\n * ```\n */\nexport function useNamespacedTranslations(namespace: string) {\n return useNextIntlTranslations(namespace);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Formatting Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Format a date according to locale\n *\n * @example\n * ```tsx\n * const formatDate = useDateFormatter();\n * return <span>{formatDate(new Date())}</span>;\n * ```\n */\nexport function useDateFormatter() {\n const formatter = useFormatter();\n return (date: Date | number, options?: Parameters<typeof formatter.dateTime>[1]) =>\n formatter.dateTime(date, options);\n}\n\n/**\n * Format a number according to locale\n *\n * @example\n * ```tsx\n * const formatNumber = useNumberFormatter();\n * return <span>{formatNumber(1234.56)}</span>;\n * ```\n */\nexport function useNumberFormatter() {\n const formatter = useFormatter();\n return (number: number, options?: Parameters<typeof formatter.number>[1]) =>\n formatter.number(number, options);\n}\n\n/**\n * Format relative time\n *\n * @example\n * ```tsx\n * const formatRelative = useRelativeTimeFormatter();\n * return <span>{formatRelative(new Date())}</span>;\n * ```\n */\nexport function useRelativeTimeFormatter() {\n const formatter = useFormatter();\n return (date: Date | number, options?: Parameters<typeof formatter.relativeTime>[1]) =>\n formatter.relativeTime(date, options);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Locale Switching Hooks\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Locales the app declared on `I18nProvider`; the built-in routing list only\n * when it declared none.\n *\n * @example\n * ```tsx\n * const locales = useLocales();\n * // ['en', 'ru', 'ko']\n * ```\n */\nexport function useLocales(): LocaleCode[] {\n const declared = useDeclaredLocales();\n return declared ?? (routing.locales as LocaleCode[]);\n}\n\n/**\n * Get default locale from routing config\n */\nexport function useDefaultLocale(): LocaleCode {\n return routing.defaultLocale as LocaleCode;\n}\n\n/**\n * Hook to change current locale\n *\n * @example\n * ```tsx\n * const changeLocale = useChangeLocale();\n *\n * <button onClick={() => changeLocale('ru')}>\n * Switch to Russian\n * </button>\n * ```\n */\nexport function useChangeLocale() {\n const pathname = usePathname();\n\n return (locale: LocaleCode) => {\n // Set NEXT_LOCALE so middleware remembers the choice, on the registrable domain so every host of the family\n // reads it (see ./shared-locale); the host-only twin is expired so it cannot shadow the shared one.\n const secure = window.location.protocol === 'https:';\n const domain = sharedCookieDomain(window.location.hostname, process.env.NEXT_PUBLIC_COOKIE_DOMAIN);\n document.cookie = localeCookieHeader(locale, domain, secure);\n if (domain) document.cookie = expireHostOnlyLocaleCookie(secure);\n\n // Hard navigation to force full page reload with new locale\n // (ensures server components re-render with updated translations)\n window.location.href = `/${locale}${pathname}`;\n };\n}\n\n/**\n * Combined hook for locale switching\n * Returns current locale, available locales, and change function\n *\n * @example\n * ```tsx\n * const { locale, locales, changeLocale } = useLocaleSwitcher();\n *\n * <select value={locale} onChange={(e) => changeLocale(e.target.value)}>\n * {locales.map(l => <option key={l} value={l}>{l}</option>)}\n * </select>\n * ```\n */\nexport function useLocaleSwitcher() {\n const locale = useLocale();\n const locales = useLocales();\n const changeLocale = useChangeLocale();\n\n return { locale, locales, changeLocale };\n}\n","/**\n * i18n Navigation Utilities\n *\n * Provides locale-aware navigation components and functions\n *\n * @example\n * ```ts\n * // In your app's i18n/navigation.ts\n * import { createNavigation } from '@djangocfg/nextjs/i18n';\n * import { routing } from './routing';\n *\n * export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);\n * ```\n *\n * @example\n * ```tsx\n * // Using in components\n * import { Link, usePathname } from '@/i18n/navigation';\n *\n * function Nav() {\n * const pathname = usePathname();\n * return <Link href=\"/about\">About</Link>;\n * }\n * ```\n */\n\nimport { createNavigation as createNextIntlNavigation } from 'next-intl/navigation';\n\nimport { createRouting, routing } from './routing';\n\nimport type { I18nConfig } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Navigation Factory\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Create navigation utilities with custom routing\n *\n * Returns locale-aware versions of:\n * - Link: Locale-prefixed links\n * - redirect: Server-side redirect with locale\n * - usePathname: Current pathname without locale prefix\n * - useRouter: Router with locale-aware navigation\n * - getPathname: Get pathname for a route\n */\nexport function createNavigation(routingConfig?: ReturnType<typeof createRouting>) {\n const config = routingConfig ?? routing;\n return createNextIntlNavigation(config);\n}\n\n/**\n * Create navigation from config options\n */\nexport function createNavigationFromConfig(config: Partial<I18nConfig>) {\n const routingConfig = createRouting(config);\n return createNextIntlNavigation(routingConfig);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Default Navigation (using default routing)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Default navigation utilities\n * Use these directly or create custom ones with createNavigation()\n */\nexport const {\n Link,\n redirect,\n usePathname,\n useRouter,\n getPathname,\n} = createNavigation();\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Props for locale-aware Link component\n */\nexport interface LinkProps {\n /** Target href */\n href: string;\n /** Target locale (optional, defaults to current) */\n locale?: string;\n /** Children */\n children?: React.ReactNode;\n /** Additional props passed to Next.js Link */\n [key: string]: unknown;\n}\n","/**\n * i18n Routing Configuration\n *\n * Creates routing configuration for next-intl\n * Used by proxy and navigation components\n */\n\nimport { defineRouting } from 'next-intl/routing';\n\nimport type { I18nConfig, LocaleCode } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Default Configuration\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport const DEFAULT_LOCALES: LocaleCode[] = ['en', 'ru', 'ko', 'ja', 'de', 'fr', 'zh', 'it', 'es', 'nl', 'ar', 'tr', 'pt-BR', 'pl', 'sv', 'no', 'da'];\nexport const DEFAULT_LOCALE: LocaleCode = 'en';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Routing Factory\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Create routing configuration for next-intl\n *\n * @example\n * ```ts\n * // i18n/routing.ts\n * import { createRouting } from '@djangocfg/nextjs/i18n';\n *\n * export const routing = createRouting({\n * locales: ['en', 'ru', 'ko'],\n * defaultLocale: 'en',\n * });\n * ```\n */\nexport function createRouting(config?: Partial<I18nConfig>) {\n const locales = config?.locales ?? DEFAULT_LOCALES;\n const defaultLocale = config?.defaultLocale ?? DEFAULT_LOCALE;\n const localePrefix = config?.localePrefix ?? 'as-needed';\n\n return defineRouting({\n locales,\n defaultLocale,\n localePrefix,\n });\n}\n\n/**\n * Default routing configuration\n * Can be overridden by app-specific configuration\n */\nexport const routing = createRouting();\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Locale Utilities\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Check if a locale is supported\n */\nexport function isValidLocale(\n locale: string,\n supportedLocales: readonly string[] = DEFAULT_LOCALES\n): locale is LocaleCode {\n return supportedLocales.includes(locale as LocaleCode);\n}\n\n/**\n * Get locale from params (handles async params in Next.js 15+)\n */\nexport async function getLocaleFromParams(\n params: Promise<{ locale: string }> | { locale: string }\n): Promise<LocaleCode> {\n const resolved = await params;\n return resolved.locale as LocaleCode;\n}\n\n/**\n * Generate static params for all locales\n * Use in generateStaticParams for locale pages\n */\nexport function generateLocaleParams(locales: readonly string[] = DEFAULT_LOCALES) {\n return locales.map((locale) => ({ locale }));\n}\n","/**\n * i18n Provider for Next.js App Router\n *\n * Bridges next-intl with @djangocfg/i18n\n * Provides translations to both Server and Client components\n *\n * @example\n * ```tsx\n * // app/[locale]/layout.tsx\n * import { I18nProvider } from '@djangocfg/nextjs/i18n';\n * import { getMessages, getLocale } from '@djangocfg/nextjs/i18n/server';\n *\n * export default async function LocaleLayout({ children, params }) {\n * const locale = await getLocale(params);\n * const messages = await getMessages();\n *\n * return (\n * <I18nProvider locale={locale} messages={messages}>\n * {children}\n * </I18nProvider>\n * );\n * }\n * ```\n */\n\n'use client';\n\nimport { createContext, useContext } from 'react';\nimport { NextIntlClientProvider } from 'next-intl';\nimport { I18nProvider as DjangoCfgI18nProvider } from '@djangocfg/i18n';\nimport type { I18nProviderProps, LocaleCode } from './types';\n\nconst DeclaredLocalesContext = createContext<LocaleCode[] | null>(null);\n\n/** Locales the app passed to its provider, or null when it passed none. */\nexport const useDeclaredLocales = (): LocaleCode[] | null => useContext(DeclaredLocalesContext);\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Provider Component\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Unified i18n Provider\n *\n * Wraps both NextIntlClientProvider and @djangocfg/i18n's I18nProvider\n * This ensures both next-intl hooks and @djangocfg/i18n hooks work correctly\n */\nexport function I18nProvider({\n locale,\n messages,\n locales,\n timeZone = 'UTC',\n now,\n children,\n}: I18nProviderProps) {\n return (\n <NextIntlClientProvider\n locale={locale}\n messages={messages}\n timeZone={timeZone}\n now={now}\n >\n <DjangoCfgI18nProvider\n locale={locale as LocaleCode}\n translations={messages}\n >\n <DeclaredLocalesContext.Provider value={locales ?? null}>\n {children}\n </DeclaredLocalesContext.Provider>\n </DjangoCfgI18nProvider>\n </NextIntlClientProvider>\n );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Lightweight Provider (next-intl only)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Next-intl only provider (without @djangocfg/i18n context)\n * Use if you only need next-intl hooks\n */\nexport function NextIntlProvider({\n locale,\n messages,\n locales,\n timeZone = 'UTC',\n now,\n children,\n}: I18nProviderProps) {\n return (\n <NextIntlClientProvider\n locale={locale}\n messages={messages}\n timeZone={timeZone}\n now={now}\n >\n <DeclaredLocalesContext.Provider value={locales ?? null}>\n {children}\n </DeclaredLocalesContext.Provider>\n </NextIntlClientProvider>\n );\n}\n","/**\n * One locale across every host of one product family.\n *\n * `NEXT_LOCALE` is written host-only by default, so a reader who picked Russian on `hub.example.com` meets English on\n * `team.example.com`. The session tokens already live on the registrable domain (`@djangocfg/api` `createStorage`,\n * `cookieDomain: 'auto'`); the locale follows the same rule: the cookie gets `Domain=` the registrable domain, and the\n * host-only twin is expired so it cannot shadow the shared one.\n *\n * Pure and framework-free, so the proxy (server) and `useChangeLocale` (browser) agree on the domain.\n */\n\nexport const LOCALE_COOKIE = 'NEXT_LOCALE';\nexport const LOCALE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;\n\n// Second-level public suffixes whose registrable domain has three labels. Not the whole Public Suffix List: the common\n// ones; anything else sets NEXT_PUBLIC_COOKIE_DOMAIN.\nconst TWO_LABEL_SUFFIXES = new Set([\n 'co.uk', 'org.uk', 'ac.uk', 'com.au', 'net.au', 'co.nz', 'co.jp', 'co.kr', 'or.kr', 'com.br', 'com.tr', 'com.cn', 'com.mx',\n 'co.in', 'co.za', 'com.sg', 'com.hk', 'com.tw', 'com.ar', 'com.ua', 'co.il',\n]);\n\n/**\n * The domain a cookie must carry to be sent to every subdomain, or `null` for a host that cannot share (an IP, a bare\n * label such as `localhost`, or an apex with no subdomain in use: a host-only cookie already covers it).\n * `override` (`NEXT_PUBLIC_COOKIE_DOMAIN`) wins when set.\n */\nexport function sharedCookieDomain(hostname: string, override?: string | null): string | null {\n const forced = override?.trim().replace(/^\\./, '');\n if (forced) return forced;\n const host = hostname.split(':')[0]!.toLowerCase();\n if (!host || /^\\d+(\\.\\d+){3}$/.test(host) || host.includes(':')) return null;\n const labels = host.split('.');\n if (labels.length < 2) return null;\n const keep = TWO_LABEL_SUFFIXES.has(labels.slice(-2).join('.')) ? 3 : 2;\n return labels.slice(-keep).join('.');\n}\n\n/** The `Set-Cookie` value that writes the shared locale cookie. */\nexport function localeCookieHeader(locale: string, domain: string | null, secure: boolean): string {\n return `${LOCALE_COOKIE}=${encodeURIComponent(locale)}; Path=/; Max-Age=${LOCALE_COOKIE_MAX_AGE}; SameSite=Lax${domain ? `; Domain=${domain}` : ''}${secure ? '; Secure' : ''}`;\n}\n\n/** Expires the host-only twin of the cookie (a cookie is identified by name and Domain). */\nexport function expireHostOnlyLocaleCookie(secure: boolean): string {\n return `${LOCALE_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax${secure ? '; Secure' : ''}`;\n}\n"],"mappings":"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","names":["jsx"]}
@@ -13,3 +13,27 @@ import './routing.mjs';
13
13
  import 'next-intl/routing';
14
14
  import 'next-intl/server';
15
15
  import '@djangocfg/layouts';
16
+
17
+ /**
18
+ * One locale across every host of one product family.
19
+ *
20
+ * `NEXT_LOCALE` is written host-only by default, so a reader who picked Russian on `hub.example.com` meets English on
21
+ * `team.example.com`. The session tokens already live on the registrable domain (`@djangocfg/api` `createStorage`,
22
+ * `cookieDomain: 'auto'`); the locale follows the same rule: the cookie gets `Domain=` the registrable domain, and the
23
+ * host-only twin is expired so it cannot shadow the shared one.
24
+ *
25
+ * Pure and framework-free, so the proxy (server) and `useChangeLocale` (browser) agree on the domain.
26
+ */
27
+ declare const LOCALE_COOKIE = "NEXT_LOCALE";
28
+ /**
29
+ * The domain a cookie must carry to be sent to every subdomain, or `null` for a host that cannot share (an IP, a bare
30
+ * label such as `localhost`, or an apex with no subdomain in use: a host-only cookie already covers it).
31
+ * `override` (`NEXT_PUBLIC_COOKIE_DOMAIN`) wins when set.
32
+ */
33
+ declare function sharedCookieDomain(hostname: string, override?: string | null): string | null;
34
+ /** The `Set-Cookie` value that writes the shared locale cookie. */
35
+ declare function localeCookieHeader(locale: string, domain: string | null, secure: boolean): string;
36
+ /** Expires the host-only twin of the cookie (a cookie is identified by name and Domain). */
37
+ declare function expireHostOnlyLocaleCookie(secure: boolean): string;
38
+
39
+ export { LOCALE_COOKIE, expireHostOnlyLocaleCookie, localeCookieHeader, sharedCookieDomain };
@@ -1 +1,49 @@
1
+ // src/i18n/shared-locale.ts
2
+ var LOCALE_COOKIE = "NEXT_LOCALE";
3
+ var LOCALE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
4
+ var TWO_LABEL_SUFFIXES = /* @__PURE__ */ new Set([
5
+ "co.uk",
6
+ "org.uk",
7
+ "ac.uk",
8
+ "com.au",
9
+ "net.au",
10
+ "co.nz",
11
+ "co.jp",
12
+ "co.kr",
13
+ "or.kr",
14
+ "com.br",
15
+ "com.tr",
16
+ "com.cn",
17
+ "com.mx",
18
+ "co.in",
19
+ "co.za",
20
+ "com.sg",
21
+ "com.hk",
22
+ "com.tw",
23
+ "com.ar",
24
+ "com.ua",
25
+ "co.il"
26
+ ]);
27
+ function sharedCookieDomain(hostname, override) {
28
+ const forced = override?.trim().replace(/^\./, "");
29
+ if (forced) return forced;
30
+ const host = hostname.split(":")[0].toLowerCase();
31
+ if (!host || /^\d+(\.\d+){3}$/.test(host) || host.includes(":")) return null;
32
+ const labels = host.split(".");
33
+ if (labels.length < 2) return null;
34
+ const keep = TWO_LABEL_SUFFIXES.has(labels.slice(-2).join(".")) ? 3 : 2;
35
+ return labels.slice(-keep).join(".");
36
+ }
37
+ function localeCookieHeader(locale, domain, secure) {
38
+ return `${LOCALE_COOKIE}=${encodeURIComponent(locale)}; Path=/; Max-Age=${LOCALE_COOKIE_MAX_AGE}; SameSite=Lax${domain ? `; Domain=${domain}` : ""}${secure ? "; Secure" : ""}`;
39
+ }
40
+ function expireHostOnlyLocaleCookie(secure) {
41
+ return `${LOCALE_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax${secure ? "; Secure" : ""}`;
42
+ }
43
+ export {
44
+ LOCALE_COOKIE,
45
+ expireHostOnlyLocaleCookie,
46
+ localeCookieHeader,
47
+ sharedCookieDomain
48
+ };
1
49
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
1
+ {"version":3,"sources":["../../src/i18n/shared-locale.ts"],"sourcesContent":["/**\n * One locale across every host of one product family.\n *\n * `NEXT_LOCALE` is written host-only by default, so a reader who picked Russian on `hub.example.com` meets English on\n * `team.example.com`. The session tokens already live on the registrable domain (`@djangocfg/api` `createStorage`,\n * `cookieDomain: 'auto'`); the locale follows the same rule: the cookie gets `Domain=` the registrable domain, and the\n * host-only twin is expired so it cannot shadow the shared one.\n *\n * Pure and framework-free, so the proxy (server) and `useChangeLocale` (browser) agree on the domain.\n */\n\nexport const LOCALE_COOKIE = 'NEXT_LOCALE';\nexport const LOCALE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;\n\n// Second-level public suffixes whose registrable domain has three labels. Not the whole Public Suffix List: the common\n// ones; anything else sets NEXT_PUBLIC_COOKIE_DOMAIN.\nconst TWO_LABEL_SUFFIXES = new Set([\n 'co.uk', 'org.uk', 'ac.uk', 'com.au', 'net.au', 'co.nz', 'co.jp', 'co.kr', 'or.kr', 'com.br', 'com.tr', 'com.cn', 'com.mx',\n 'co.in', 'co.za', 'com.sg', 'com.hk', 'com.tw', 'com.ar', 'com.ua', 'co.il',\n]);\n\n/**\n * The domain a cookie must carry to be sent to every subdomain, or `null` for a host that cannot share (an IP, a bare\n * label such as `localhost`, or an apex with no subdomain in use: a host-only cookie already covers it).\n * `override` (`NEXT_PUBLIC_COOKIE_DOMAIN`) wins when set.\n */\nexport function sharedCookieDomain(hostname: string, override?: string | null): string | null {\n const forced = override?.trim().replace(/^\\./, '');\n if (forced) return forced;\n const host = hostname.split(':')[0]!.toLowerCase();\n if (!host || /^\\d+(\\.\\d+){3}$/.test(host) || host.includes(':')) return null;\n const labels = host.split('.');\n if (labels.length < 2) return null;\n const keep = TWO_LABEL_SUFFIXES.has(labels.slice(-2).join('.')) ? 3 : 2;\n return labels.slice(-keep).join('.');\n}\n\n/** The `Set-Cookie` value that writes the shared locale cookie. */\nexport function localeCookieHeader(locale: string, domain: string | null, secure: boolean): string {\n return `${LOCALE_COOKIE}=${encodeURIComponent(locale)}; Path=/; Max-Age=${LOCALE_COOKIE_MAX_AGE}; SameSite=Lax${domain ? `; Domain=${domain}` : ''}${secure ? '; Secure' : ''}`;\n}\n\n/** Expires the host-only twin of the cookie (a cookie is identified by name and Domain). */\nexport function expireHostOnlyLocaleCookie(secure: boolean): string {\n return `${LOCALE_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax${secure ? '; Secure' : ''}`;\n}\n"],"mappings":";AAWO,IAAM,gBAAgB;AACtB,IAAM,wBAAwB,KAAK,KAAK,KAAK;AAIpD,IAAM,qBAAqB,oBAAI,IAAI;AAAA,EACjC;AAAA,EAAS;AAAA,EAAU;AAAA,EAAS;AAAA,EAAU;AAAA,EAAU;AAAA,EAAS;AAAA,EAAS;AAAA,EAAS;AAAA,EAAS;AAAA,EAAU;AAAA,EAAU;AAAA,EAAU;AAAA,EAClH;AAAA,EAAS;AAAA,EAAS;AAAA,EAAU;AAAA,EAAU;AAAA,EAAU;AAAA,EAAU;AAAA,EAAU;AACtE,CAAC;AAOM,SAAS,mBAAmB,UAAkB,UAAyC;AAC5F,QAAM,SAAS,UAAU,KAAK,EAAE,QAAQ,OAAO,EAAE;AACjD,MAAI,OAAQ,QAAO;AACnB,QAAM,OAAO,SAAS,MAAM,GAAG,EAAE,CAAC,EAAG,YAAY;AACjD,MAAI,CAAC,QAAQ,kBAAkB,KAAK,IAAI,KAAK,KAAK,SAAS,GAAG,EAAG,QAAO;AACxE,QAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,MAAI,OAAO,SAAS,EAAG,QAAO;AAC9B,QAAM,OAAO,mBAAmB,IAAI,OAAO,MAAM,EAAE,EAAE,KAAK,GAAG,CAAC,IAAI,IAAI;AACtE,SAAO,OAAO,MAAM,CAAC,IAAI,EAAE,KAAK,GAAG;AACrC;AAGO,SAAS,mBAAmB,QAAgB,QAAuB,QAAyB;AACjG,SAAO,GAAG,aAAa,IAAI,mBAAmB,MAAM,CAAC,qBAAqB,qBAAqB,iBAAiB,SAAS,YAAY,MAAM,KAAK,EAAE,GAAG,SAAS,aAAa,EAAE;AAC/K;AAGO,SAAS,2BAA2B,QAAyB;AAClE,SAAO,GAAG,aAAa,qCAAqC,SAAS,aAAa,EAAE;AACtF;","names":[]}
@@ -1,18 +1,42 @@
1
- import * as next_server from 'next/server';
2
- import { NextRequest } from 'next/server';
3
1
  import { createRouting } from './routing.mjs';
2
+ import { NextRequest, NextResponse } from 'next/server';
4
3
  import { I18nConfig } from './index.mjs';
5
4
  import 'next-intl/routing';
6
5
  import '@djangocfg/i18n';
7
6
 
7
+ /**
8
+ * i18n Proxy for Next.js App Router (Next.js 16+)
9
+ *
10
+ * Handles locale detection and routing
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // proxy.ts
15
+ * export { proxy as default, config } from '@djangocfg/nextjs/i18n';
16
+ *
17
+ * // Or with custom routing:
18
+ * import { createProxy } from '@djangocfg/nextjs/i18n';
19
+ * import { routing } from './i18n/routing';
20
+ *
21
+ * export default createProxy(routing);
22
+ * export const config = { matcher: [...] };
23
+ * ```
24
+ */
25
+
26
+ /**
27
+ * next-intl writes `NEXT_LOCALE` host-only. Rewrite it onto the registrable domain, so the locale a reader chose on one host
28
+ * of the family is the locale on all of them, and expire the host-only twin that would shadow it. A host that cannot share
29
+ * (localhost, an IP) is left as next-intl wrote it. `NEXT_PUBLIC_COOKIE_DOMAIN` overrides the guess.
30
+ */
31
+ declare function shareLocaleCookie(request: NextRequest, response: NextResponse): NextResponse;
8
32
  /**
9
33
  * Create i18n proxy handler with custom routing configuration
10
34
  */
11
- declare function createProxy(routingConfig?: ReturnType<typeof createRouting>): (request: NextRequest) => next_server.NextResponse<unknown>;
35
+ declare function createProxy(routingConfig?: ReturnType<typeof createRouting>): (request: NextRequest) => NextResponse<unknown>;
12
36
  /**
13
37
  * Create i18n proxy from config options
14
38
  */
15
- declare function createProxyFromConfig(config: Partial<I18nConfig>): (request: NextRequest) => next_server.NextResponse<unknown>;
39
+ declare function createProxyFromConfig(config: Partial<I18nConfig>): (request: NextRequest) => NextResponse<unknown>;
16
40
  /**
17
41
  * Default proxy function using default routing
18
42
  *
@@ -22,7 +46,7 @@ declare function createProxyFromConfig(config: Partial<I18nConfig>): (request: N
22
46
  * export { proxy as default, config } from '@djangocfg/nextjs/i18n';
23
47
  * ```
24
48
  */
25
- declare function proxy(request: NextRequest): next_server.NextResponse<unknown>;
49
+ declare function proxy(request: NextRequest): NextResponse<unknown>;
26
50
  /**
27
51
  * Default proxy config
28
52
  * Matches all paths except static files, API routes, and Next.js internals
@@ -39,4 +63,4 @@ declare const config: {
39
63
  matcher: string[];
40
64
  };
41
65
 
42
- export { config, createProxy, createProxyFromConfig, proxy };
66
+ export { config, createProxy, createProxyFromConfig, proxy, shareLocaleCookie };
@@ -17,14 +17,65 @@ function createRouting(config2) {
17
17
  }
18
18
  var routing = createRouting();
19
19
 
20
+ // src/i18n/shared-locale.ts
21
+ var LOCALE_COOKIE = "NEXT_LOCALE";
22
+ var LOCALE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
23
+ var TWO_LABEL_SUFFIXES = /* @__PURE__ */ new Set([
24
+ "co.uk",
25
+ "org.uk",
26
+ "ac.uk",
27
+ "com.au",
28
+ "net.au",
29
+ "co.nz",
30
+ "co.jp",
31
+ "co.kr",
32
+ "or.kr",
33
+ "com.br",
34
+ "com.tr",
35
+ "com.cn",
36
+ "com.mx",
37
+ "co.in",
38
+ "co.za",
39
+ "com.sg",
40
+ "com.hk",
41
+ "com.tw",
42
+ "com.ar",
43
+ "com.ua",
44
+ "co.il"
45
+ ]);
46
+ function sharedCookieDomain(hostname, override) {
47
+ const forced = override?.trim().replace(/^\./, "");
48
+ if (forced) return forced;
49
+ const host = hostname.split(":")[0].toLowerCase();
50
+ if (!host || /^\d+(\.\d+){3}$/.test(host) || host.includes(":")) return null;
51
+ const labels = host.split(".");
52
+ if (labels.length < 2) return null;
53
+ const keep = TWO_LABEL_SUFFIXES.has(labels.slice(-2).join(".")) ? 3 : 2;
54
+ return labels.slice(-keep).join(".");
55
+ }
56
+ function expireHostOnlyLocaleCookie(secure) {
57
+ return `${LOCALE_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax${secure ? "; Secure" : ""}`;
58
+ }
59
+
20
60
  // src/i18n/proxy.ts
61
+ function shareLocaleCookie(request, response) {
62
+ const written = response.cookies.get(LOCALE_COOKIE);
63
+ if (!written) return response;
64
+ const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host") ?? request.nextUrl.hostname;
65
+ const domain = sharedCookieDomain(host, process.env.NEXT_PUBLIC_COOKIE_DOMAIN);
66
+ if (!domain) return response;
67
+ const secure = (request.headers.get("x-forwarded-proto") ?? request.nextUrl.protocol.replace(":", "")) === "https";
68
+ response.cookies.set({ name: LOCALE_COOKIE, value: written.value, domain, path: "/", maxAge: LOCALE_COOKIE_MAX_AGE, sameSite: "lax", secure });
69
+ response.headers.append("set-cookie", expireHostOnlyLocaleCookie(secure));
70
+ return response;
71
+ }
21
72
  function createProxy(routingConfig) {
22
73
  const config2 = routingConfig ?? routing;
23
- return createIntlMiddleware(config2);
74
+ const intl = createIntlMiddleware(config2);
75
+ return (request) => shareLocaleCookie(request, intl(request));
24
76
  }
25
77
  function createProxyFromConfig(config2) {
26
- const routingConfig = createRouting(config2);
27
- return createIntlMiddleware(routingConfig);
78
+ return createProxy(createRouting(config2));
28
79
  }
29
80
  var handleI18nRouting = createProxy();
30
81
  function proxy(request) {
@@ -39,6 +90,7 @@ export {
39
90
  config,
40
91
  createProxy,
41
92
  createProxyFromConfig,
42
- proxy
93
+ proxy,
94
+ shareLocaleCookie
43
95
  };
44
96
  //# sourceMappingURL=proxy.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/i18n/proxy.ts","../../src/i18n/routing.ts"],"sourcesContent":["/**\n * i18n Proxy for Next.js App Router (Next.js 16+)\n *\n * Handles locale detection and routing\n *\n * @example\n * ```ts\n * // proxy.ts\n * export { proxy as default, config } from '@djangocfg/nextjs/i18n';\n *\n * // Or with custom routing:\n * import { createProxy } from '@djangocfg/nextjs/i18n';\n * import { routing } from './i18n/routing';\n *\n * export default createProxy(routing);\n * export const config = { matcher: [...] };\n * ```\n */\n\nimport createIntlMiddleware from 'next-intl/middleware';\n\nimport { createRouting, routing } from './routing';\n\nimport type { NextRequest } from 'next/server';\n\nimport type { I18nConfig } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Proxy Factory\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Create i18n proxy handler with custom routing configuration\n */\nexport function createProxy(routingConfig?: ReturnType<typeof createRouting>) {\n const config = routingConfig ?? routing;\n return createIntlMiddleware(config);\n}\n\n/**\n * Create i18n proxy from config options\n */\nexport function createProxyFromConfig(config: Partial<I18nConfig>) {\n const routingConfig = createRouting(config);\n return createIntlMiddleware(routingConfig);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Default Proxy\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst handleI18nRouting = createProxy();\n\n/**\n * Default proxy function using default routing\n *\n * @example\n * ```ts\n * // proxy.ts\n * export { proxy as default, config } from '@djangocfg/nextjs/i18n';\n * ```\n */\nexport function proxy(request: NextRequest) {\n return handleI18nRouting(request);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Config\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Default proxy config\n * Matches all paths except static files, API routes, and Next.js internals\n *\n * Uses explicit _next/static and _next/image exclusions (required for Next.js 16 / Turbopack)\n *\n * @example\n * ```ts\n * // proxy.ts\n * export { proxy as default, config } from '@djangocfg/nextjs/i18n';\n * ```\n */\nexport const config = {\n matcher: [\n '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',\n ],\n};\n","/**\n * i18n Routing Configuration\n *\n * Creates routing configuration for next-intl\n * Used by proxy and navigation components\n */\n\nimport { defineRouting } from 'next-intl/routing';\n\nimport type { I18nConfig, LocaleCode } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Default Configuration\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport const DEFAULT_LOCALES: LocaleCode[] = ['en', 'ru', 'ko', 'ja', 'de', 'fr', 'zh', 'it', 'es', 'nl', 'ar', 'tr', 'pt-BR', 'pl', 'sv', 'no', 'da'];\nexport const DEFAULT_LOCALE: LocaleCode = 'en';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Routing Factory\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Create routing configuration for next-intl\n *\n * @example\n * ```ts\n * // i18n/routing.ts\n * import { createRouting } from '@djangocfg/nextjs/i18n';\n *\n * export const routing = createRouting({\n * locales: ['en', 'ru', 'ko'],\n * defaultLocale: 'en',\n * });\n * ```\n */\nexport function createRouting(config?: Partial<I18nConfig>) {\n const locales = config?.locales ?? DEFAULT_LOCALES;\n const defaultLocale = config?.defaultLocale ?? DEFAULT_LOCALE;\n const localePrefix = config?.localePrefix ?? 'as-needed';\n\n return defineRouting({\n locales,\n defaultLocale,\n localePrefix,\n });\n}\n\n/**\n * Default routing configuration\n * Can be overridden by app-specific configuration\n */\nexport const routing = createRouting();\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Locale Utilities\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Check if a locale is supported\n */\nexport function isValidLocale(\n locale: string,\n supportedLocales: readonly string[] = DEFAULT_LOCALES\n): locale is LocaleCode {\n return supportedLocales.includes(locale as LocaleCode);\n}\n\n/**\n * Get locale from params (handles async params in Next.js 15+)\n */\nexport async function getLocaleFromParams(\n params: Promise<{ locale: string }> | { locale: string }\n): Promise<LocaleCode> {\n const resolved = await params;\n return resolved.locale as LocaleCode;\n}\n\n/**\n * Generate static params for all locales\n * Use in generateStaticParams for locale pages\n */\nexport function generateLocaleParams(locales: readonly string[] = DEFAULT_LOCALES) {\n return locales.map((locale) => ({ locale }));\n}\n"],"mappings":";AAmBA,OAAO,0BAA0B;;;ACZjC,SAAS,qBAAqB;AAQvB,IAAM,kBAAgC,CAAC,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,SAAS,MAAM,MAAM,MAAM,IAAI;AAC9I,IAAM,iBAA6B;AAoBnC,SAAS,cAAcA,SAA8B;AAC1D,QAAM,UAAUA,SAAQ,WAAW;AACnC,QAAM,gBAAgBA,SAAQ,iBAAiB;AAC/C,QAAM,eAAeA,SAAQ,gBAAgB;AAE7C,SAAO,cAAc;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAMO,IAAM,UAAU,cAAc;;;ADlB9B,SAAS,YAAY,eAAkD;AAC5E,QAAMC,UAAS,iBAAiB;AAChC,SAAO,qBAAqBA,OAAM;AACpC;AAKO,SAAS,sBAAsBA,SAA6B;AACjE,QAAM,gBAAgB,cAAcA,OAAM;AAC1C,SAAO,qBAAqB,aAAa;AAC3C;AAMA,IAAM,oBAAoB,YAAY;AAW/B,SAAS,MAAM,SAAsB;AAC1C,SAAO,kBAAkB,OAAO;AAClC;AAkBO,IAAM,SAAS;AAAA,EACpB,SAAS;AAAA,IACP;AAAA,EACF;AACF;","names":["config","config"]}
1
+ {"version":3,"sources":["../../src/i18n/proxy.ts","../../src/i18n/routing.ts","../../src/i18n/shared-locale.ts"],"sourcesContent":["/**\n * i18n Proxy for Next.js App Router (Next.js 16+)\n *\n * Handles locale detection and routing\n *\n * @example\n * ```ts\n * // proxy.ts\n * export { proxy as default, config } from '@djangocfg/nextjs/i18n';\n *\n * // Or with custom routing:\n * import { createProxy } from '@djangocfg/nextjs/i18n';\n * import { routing } from './i18n/routing';\n *\n * export default createProxy(routing);\n * export const config = { matcher: [...] };\n * ```\n */\n\nimport createIntlMiddleware from 'next-intl/middleware';\n\nimport { createRouting, routing } from './routing';\nimport { expireHostOnlyLocaleCookie, LOCALE_COOKIE, LOCALE_COOKIE_MAX_AGE, sharedCookieDomain } from './shared-locale';\n\nimport type { NextRequest, NextResponse } from 'next/server';\n\nimport type { I18nConfig } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Proxy Factory\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * next-intl writes `NEXT_LOCALE` host-only. Rewrite it onto the registrable domain, so the locale a reader chose on one host\n * of the family is the locale on all of them, and expire the host-only twin that would shadow it. A host that cannot share\n * (localhost, an IP) is left as next-intl wrote it. `NEXT_PUBLIC_COOKIE_DOMAIN` overrides the guess.\n */\nexport function shareLocaleCookie(request: NextRequest, response: NextResponse): NextResponse {\n const written = response.cookies.get(LOCALE_COOKIE);\n if (!written) return response;\n const host = request.headers.get('x-forwarded-host') ?? request.headers.get('host') ?? request.nextUrl.hostname;\n const domain = sharedCookieDomain(host, process.env.NEXT_PUBLIC_COOKIE_DOMAIN);\n if (!domain) return response;\n const secure = (request.headers.get('x-forwarded-proto') ?? request.nextUrl.protocol.replace(':', '')) === 'https';\n response.cookies.set({ name: LOCALE_COOKIE, value: written.value, domain, path: '/', maxAge: LOCALE_COOKIE_MAX_AGE, sameSite: 'lax', secure });\n response.headers.append('set-cookie', expireHostOnlyLocaleCookie(secure));\n return response;\n}\n\n/**\n * Create i18n proxy handler with custom routing configuration\n */\nexport function createProxy(routingConfig?: ReturnType<typeof createRouting>) {\n const config = routingConfig ?? routing;\n const intl = createIntlMiddleware(config);\n return (request: NextRequest) => shareLocaleCookie(request, intl(request) as NextResponse);\n}\n\n/**\n * Create i18n proxy from config options\n */\nexport function createProxyFromConfig(config: Partial<I18nConfig>) {\n return createProxy(createRouting(config));\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Default Proxy\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst handleI18nRouting = createProxy();\n\n/**\n * Default proxy function using default routing\n *\n * @example\n * ```ts\n * // proxy.ts\n * export { proxy as default, config } from '@djangocfg/nextjs/i18n';\n * ```\n */\nexport function proxy(request: NextRequest) {\n return handleI18nRouting(request);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Config\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Default proxy config\n * Matches all paths except static files, API routes, and Next.js internals\n *\n * Uses explicit _next/static and _next/image exclusions (required for Next.js 16 / Turbopack)\n *\n * @example\n * ```ts\n * // proxy.ts\n * export { proxy as default, config } from '@djangocfg/nextjs/i18n';\n * ```\n */\nexport const config = {\n matcher: [\n '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',\n ],\n};\n","/**\n * i18n Routing Configuration\n *\n * Creates routing configuration for next-intl\n * Used by proxy and navigation components\n */\n\nimport { defineRouting } from 'next-intl/routing';\n\nimport type { I18nConfig, LocaleCode } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Default Configuration\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport const DEFAULT_LOCALES: LocaleCode[] = ['en', 'ru', 'ko', 'ja', 'de', 'fr', 'zh', 'it', 'es', 'nl', 'ar', 'tr', 'pt-BR', 'pl', 'sv', 'no', 'da'];\nexport const DEFAULT_LOCALE: LocaleCode = 'en';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Routing Factory\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Create routing configuration for next-intl\n *\n * @example\n * ```ts\n * // i18n/routing.ts\n * import { createRouting } from '@djangocfg/nextjs/i18n';\n *\n * export const routing = createRouting({\n * locales: ['en', 'ru', 'ko'],\n * defaultLocale: 'en',\n * });\n * ```\n */\nexport function createRouting(config?: Partial<I18nConfig>) {\n const locales = config?.locales ?? DEFAULT_LOCALES;\n const defaultLocale = config?.defaultLocale ?? DEFAULT_LOCALE;\n const localePrefix = config?.localePrefix ?? 'as-needed';\n\n return defineRouting({\n locales,\n defaultLocale,\n localePrefix,\n });\n}\n\n/**\n * Default routing configuration\n * Can be overridden by app-specific configuration\n */\nexport const routing = createRouting();\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Locale Utilities\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Check if a locale is supported\n */\nexport function isValidLocale(\n locale: string,\n supportedLocales: readonly string[] = DEFAULT_LOCALES\n): locale is LocaleCode {\n return supportedLocales.includes(locale as LocaleCode);\n}\n\n/**\n * Get locale from params (handles async params in Next.js 15+)\n */\nexport async function getLocaleFromParams(\n params: Promise<{ locale: string }> | { locale: string }\n): Promise<LocaleCode> {\n const resolved = await params;\n return resolved.locale as LocaleCode;\n}\n\n/**\n * Generate static params for all locales\n * Use in generateStaticParams for locale pages\n */\nexport function generateLocaleParams(locales: readonly string[] = DEFAULT_LOCALES) {\n return locales.map((locale) => ({ locale }));\n}\n","/**\n * One locale across every host of one product family.\n *\n * `NEXT_LOCALE` is written host-only by default, so a reader who picked Russian on `hub.example.com` meets English on\n * `team.example.com`. The session tokens already live on the registrable domain (`@djangocfg/api` `createStorage`,\n * `cookieDomain: 'auto'`); the locale follows the same rule: the cookie gets `Domain=` the registrable domain, and the\n * host-only twin is expired so it cannot shadow the shared one.\n *\n * Pure and framework-free, so the proxy (server) and `useChangeLocale` (browser) agree on the domain.\n */\n\nexport const LOCALE_COOKIE = 'NEXT_LOCALE';\nexport const LOCALE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;\n\n// Second-level public suffixes whose registrable domain has three labels. Not the whole Public Suffix List: the common\n// ones; anything else sets NEXT_PUBLIC_COOKIE_DOMAIN.\nconst TWO_LABEL_SUFFIXES = new Set([\n 'co.uk', 'org.uk', 'ac.uk', 'com.au', 'net.au', 'co.nz', 'co.jp', 'co.kr', 'or.kr', 'com.br', 'com.tr', 'com.cn', 'com.mx',\n 'co.in', 'co.za', 'com.sg', 'com.hk', 'com.tw', 'com.ar', 'com.ua', 'co.il',\n]);\n\n/**\n * The domain a cookie must carry to be sent to every subdomain, or `null` for a host that cannot share (an IP, a bare\n * label such as `localhost`, or an apex with no subdomain in use: a host-only cookie already covers it).\n * `override` (`NEXT_PUBLIC_COOKIE_DOMAIN`) wins when set.\n */\nexport function sharedCookieDomain(hostname: string, override?: string | null): string | null {\n const forced = override?.trim().replace(/^\\./, '');\n if (forced) return forced;\n const host = hostname.split(':')[0]!.toLowerCase();\n if (!host || /^\\d+(\\.\\d+){3}$/.test(host) || host.includes(':')) return null;\n const labels = host.split('.');\n if (labels.length < 2) return null;\n const keep = TWO_LABEL_SUFFIXES.has(labels.slice(-2).join('.')) ? 3 : 2;\n return labels.slice(-keep).join('.');\n}\n\n/** The `Set-Cookie` value that writes the shared locale cookie. */\nexport function localeCookieHeader(locale: string, domain: string | null, secure: boolean): string {\n return `${LOCALE_COOKIE}=${encodeURIComponent(locale)}; Path=/; Max-Age=${LOCALE_COOKIE_MAX_AGE}; SameSite=Lax${domain ? `; Domain=${domain}` : ''}${secure ? '; Secure' : ''}`;\n}\n\n/** Expires the host-only twin of the cookie (a cookie is identified by name and Domain). */\nexport function expireHostOnlyLocaleCookie(secure: boolean): string {\n return `${LOCALE_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax${secure ? '; Secure' : ''}`;\n}\n"],"mappings":";AAmBA,OAAO,0BAA0B;;;ACZjC,SAAS,qBAAqB;AAQvB,IAAM,kBAAgC,CAAC,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,SAAS,MAAM,MAAM,MAAM,IAAI;AAC9I,IAAM,iBAA6B;AAoBnC,SAAS,cAAcA,SAA8B;AAC1D,QAAM,UAAUA,SAAQ,WAAW;AACnC,QAAM,gBAAgBA,SAAQ,iBAAiB;AAC/C,QAAM,eAAeA,SAAQ,gBAAgB;AAE7C,SAAO,cAAc;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAMO,IAAM,UAAU,cAAc;;;ACzC9B,IAAM,gBAAgB;AACtB,IAAM,wBAAwB,KAAK,KAAK,KAAK;AAIpD,IAAM,qBAAqB,oBAAI,IAAI;AAAA,EACjC;AAAA,EAAS;AAAA,EAAU;AAAA,EAAS;AAAA,EAAU;AAAA,EAAU;AAAA,EAAS;AAAA,EAAS;AAAA,EAAS;AAAA,EAAS;AAAA,EAAU;AAAA,EAAU;AAAA,EAAU;AAAA,EAClH;AAAA,EAAS;AAAA,EAAS;AAAA,EAAU;AAAA,EAAU;AAAA,EAAU;AAAA,EAAU;AAAA,EAAU;AACtE,CAAC;AAOM,SAAS,mBAAmB,UAAkB,UAAyC;AAC5F,QAAM,SAAS,UAAU,KAAK,EAAE,QAAQ,OAAO,EAAE;AACjD,MAAI,OAAQ,QAAO;AACnB,QAAM,OAAO,SAAS,MAAM,GAAG,EAAE,CAAC,EAAG,YAAY;AACjD,MAAI,CAAC,QAAQ,kBAAkB,KAAK,IAAI,KAAK,KAAK,SAAS,GAAG,EAAG,QAAO;AACxE,QAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,MAAI,OAAO,SAAS,EAAG,QAAO;AAC9B,QAAM,OAAO,mBAAmB,IAAI,OAAO,MAAM,EAAE,EAAE,KAAK,GAAG,CAAC,IAAI,IAAI;AACtE,SAAO,OAAO,MAAM,CAAC,IAAI,EAAE,KAAK,GAAG;AACrC;AAQO,SAAS,2BAA2B,QAAyB;AAClE,SAAO,GAAG,aAAa,qCAAqC,SAAS,aAAa,EAAE;AACtF;;;AFRO,SAAS,kBAAkB,SAAsB,UAAsC;AAC5F,QAAM,UAAU,SAAS,QAAQ,IAAI,aAAa;AAClD,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,OAAO,QAAQ,QAAQ,IAAI,kBAAkB,KAAK,QAAQ,QAAQ,IAAI,MAAM,KAAK,QAAQ,QAAQ;AACvG,QAAM,SAAS,mBAAmB,MAAM,QAAQ,IAAI,yBAAyB;AAC7E,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,UAAU,QAAQ,QAAQ,IAAI,mBAAmB,KAAK,QAAQ,QAAQ,SAAS,QAAQ,KAAK,EAAE,OAAO;AAC3G,WAAS,QAAQ,IAAI,EAAE,MAAM,eAAe,OAAO,QAAQ,OAAO,QAAQ,MAAM,KAAK,QAAQ,uBAAuB,UAAU,OAAO,OAAO,CAAC;AAC7I,WAAS,QAAQ,OAAO,cAAc,2BAA2B,MAAM,CAAC;AACxE,SAAO;AACT;AAKO,SAAS,YAAY,eAAkD;AAC5E,QAAMC,UAAS,iBAAiB;AAChC,QAAM,OAAO,qBAAqBA,OAAM;AACxC,SAAO,CAAC,YAAyB,kBAAkB,SAAS,KAAK,OAAO,CAAiB;AAC3F;AAKO,SAAS,sBAAsBA,SAA6B;AACjE,SAAO,YAAY,cAAcA,OAAM,CAAC;AAC1C;AAMA,IAAM,oBAAoB,YAAY;AAW/B,SAAS,MAAM,SAAsB;AAC1C,SAAO,kBAAkB,OAAO;AAClC;AAkBO,IAAM,SAAS;AAAA,EACpB,SAAS;AAAA,IACP;AAAA,EACF;AACF;","names":["config","config"]}
@@ -24,7 +24,6 @@ declare function createRouting(config?: Partial<I18nConfig>): {
24
24
  localePrefix?: next_intl_routing.LocalePrefix<string[], "always" | "as-needed" | "never"> | undefined;
25
25
  domains?: undefined;
26
26
  localeCookie?: boolean | {
27
- name?: string | undefined;
28
27
  maxAge?: number | undefined | undefined;
29
28
  priority?: "low" | "medium" | "high" | undefined | undefined;
30
29
  domain?: string | undefined | undefined;
@@ -32,6 +31,7 @@ declare function createRouting(config?: Partial<I18nConfig>): {
32
31
  secure?: boolean | undefined | undefined;
33
32
  sameSite?: true | false | "lax" | "strict" | "none" | undefined | undefined;
34
33
  partitioned?: boolean | undefined | undefined;
34
+ name?: string | undefined;
35
35
  };
36
36
  alternateLinks?: boolean;
37
37
  localeDetection?: boolean;
@@ -46,7 +46,6 @@ declare const routing: {
46
46
  localePrefix?: next_intl_routing.LocalePrefix<string[], "always" | "as-needed" | "never"> | undefined;
47
47
  domains?: undefined;
48
48
  localeCookie?: boolean | {
49
- name?: string | undefined;
50
49
  maxAge?: number | undefined | undefined;
51
50
  priority?: "low" | "medium" | "high" | undefined | undefined;
52
51
  domain?: string | undefined | undefined;
@@ -54,6 +53,7 @@ declare const routing: {
54
53
  secure?: boolean | undefined | undefined;
55
54
  sameSite?: true | false | "lax" | "strict" | "none" | undefined | undefined;
56
55
  partitioned?: boolean | undefined | undefined;
56
+ name?: string | undefined;
57
57
  };
58
58
  alternateLinks?: boolean;
59
59
  localeDetection?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/nextjs",
3
- "version": "2.1.626",
3
+ "version": "2.1.627",
4
4
  "description": "Next.js server utilities: sitemap, health, OG images, contact forms, navigation, config",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -162,10 +162,10 @@
162
162
  "ai-docs": "tsx src/ai/cli.ts"
163
163
  },
164
164
  "peerDependencies": {
165
- "@djangocfg/api": "^2.1.626",
166
- "@djangocfg/devtools": "^2.1.626",
167
- "@djangocfg/i18n": "^2.1.626",
168
- "@djangocfg/ui-core": "^2.1.626",
165
+ "@djangocfg/api": "^2.1.627",
166
+ "@djangocfg/devtools": "^2.1.627",
167
+ "@djangocfg/i18n": "^2.1.627",
168
+ "@djangocfg/ui-core": "^2.1.627",
169
169
  "next": "^16.4.5",
170
170
  "next-intl": "^4.9.1"
171
171
  },
@@ -187,12 +187,12 @@
187
187
  "semver": "^7.8.6"
188
188
  },
189
189
  "devDependencies": {
190
- "@djangocfg/api": "^2.1.626",
191
- "@djangocfg/devtools": "^2.1.626",
192
- "@djangocfg/i18n": "^2.1.626",
193
- "@djangocfg/layouts": "^2.1.626",
194
- "@djangocfg/typescript-config": "^2.1.626",
195
- "@djangocfg/ui-core": "^2.1.626",
190
+ "@djangocfg/api": "^2.1.627",
191
+ "@djangocfg/devtools": "^2.1.627",
192
+ "@djangocfg/i18n": "^2.1.627",
193
+ "@djangocfg/layouts": "^2.1.627",
194
+ "@djangocfg/typescript-config": "^2.1.627",
195
+ "@djangocfg/ui-core": "^2.1.627",
196
196
  "@types/node": "^25.9.5",
197
197
  "@types/react": "19.3.0",
198
198
  "@types/react-dom": "19.3.0",
@@ -28,6 +28,7 @@ import {
28
28
  import { usePathname } from './navigation';
29
29
  import { I18nProvider, NextIntlProvider, useDeclaredLocales } from './provider';
30
30
  import { routing } from './routing';
31
+ import { expireHostOnlyLocaleCookie, localeCookieHeader, sharedCookieDomain } from './shared-locale';
31
32
 
32
33
  // Re-export providers
33
34
  export { I18nProvider, NextIntlProvider };
@@ -198,10 +199,12 @@ export function useChangeLocale() {
198
199
  const pathname = usePathname();
199
200
 
200
201
  return (locale: LocaleCode) => {
201
- // Set NEXT_LOCALE cookie so middleware remembers the choice
202
- const date = new Date();
203
- date.setFullYear(date.getFullYear() + 1);
204
- document.cookie = `NEXT_LOCALE=${locale}; expires=${date.toUTCString()}; path=/`;
202
+ // Set NEXT_LOCALE so middleware remembers the choice, on the registrable domain so every host of the family
203
+ // reads it (see ./shared-locale); the host-only twin is expired so it cannot shadow the shared one.
204
+ const secure = window.location.protocol === 'https:';
205
+ const domain = sharedCookieDomain(window.location.hostname, process.env.NEXT_PUBLIC_COOKIE_DOMAIN);
206
+ document.cookie = localeCookieHeader(locale, domain, secure);
207
+ if (domain) document.cookie = expireHostOnlyLocaleCookie(secure);
205
208
 
206
209
  // Hard navigation to force full page reload with new locale
207
210
  // (ensures server components re-render with updated translations)
package/src/i18n/index.ts CHANGED
@@ -48,3 +48,5 @@ export type {
48
48
  export type { LinkProps } from './navigation';
49
49
  export type { RequestConfigOptions } from './request';
50
50
  export type { LocaleSwitcherProps } from './components';
51
+
52
+ export { sharedCookieDomain, localeCookieHeader, expireHostOnlyLocaleCookie, LOCALE_COOKIE } from './shared-locale';
@@ -0,0 +1,22 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { NextRequest, NextResponse } from 'next/server';
4
+ import { shareLocaleCookie } from './proxy';
5
+
6
+ test('rewrites the cookie onto the registrable domain', () => {
7
+ const request = new NextRequest('https://hub.example.com/ru', { headers: { 'x-forwarded-proto': 'https' } });
8
+ const response = NextResponse.next();
9
+ response.cookies.set('NEXT_LOCALE', 'ru', { path: '/' });
10
+ const out = shareLocaleCookie(request, response);
11
+ const lines = out.headers.getSetCookie();
12
+ assert.ok(lines.some((l) => l.startsWith('NEXT_LOCALE=ru') && l.includes('Domain=example.com')));
13
+ assert.ok(lines.some((l) => l.startsWith('NEXT_LOCALE=;') && l.includes('Max-Age=0') && !l.includes('Domain')));
14
+ });
15
+
16
+ test('localhost is left alone', () => {
17
+ const request = new NextRequest('http://localhost:3211/ru');
18
+ const response = NextResponse.next();
19
+ response.cookies.set('NEXT_LOCALE', 'ru', { path: '/' });
20
+ const out = shareLocaleCookie(request, response);
21
+ assert.equal(out.headers.getSetCookie().length, 1);
22
+ });
package/src/i18n/proxy.ts CHANGED
@@ -20,8 +20,9 @@
20
20
  import createIntlMiddleware from 'next-intl/middleware';
21
21
 
22
22
  import { createRouting, routing } from './routing';
23
+ import { expireHostOnlyLocaleCookie, LOCALE_COOKIE, LOCALE_COOKIE_MAX_AGE, sharedCookieDomain } from './shared-locale';
23
24
 
24
- import type { NextRequest } from 'next/server';
25
+ import type { NextRequest, NextResponse } from 'next/server';
25
26
 
26
27
  import type { I18nConfig } from './types';
27
28
 
@@ -29,20 +30,37 @@ import type { I18nConfig } from './types';
29
30
  // Proxy Factory
30
31
  // ─────────────────────────────────────────────────────────────────────────────
31
32
 
33
+ /**
34
+ * next-intl writes `NEXT_LOCALE` host-only. Rewrite it onto the registrable domain, so the locale a reader chose on one host
35
+ * of the family is the locale on all of them, and expire the host-only twin that would shadow it. A host that cannot share
36
+ * (localhost, an IP) is left as next-intl wrote it. `NEXT_PUBLIC_COOKIE_DOMAIN` overrides the guess.
37
+ */
38
+ export function shareLocaleCookie(request: NextRequest, response: NextResponse): NextResponse {
39
+ const written = response.cookies.get(LOCALE_COOKIE);
40
+ if (!written) return response;
41
+ const host = request.headers.get('x-forwarded-host') ?? request.headers.get('host') ?? request.nextUrl.hostname;
42
+ const domain = sharedCookieDomain(host, process.env.NEXT_PUBLIC_COOKIE_DOMAIN);
43
+ if (!domain) return response;
44
+ const secure = (request.headers.get('x-forwarded-proto') ?? request.nextUrl.protocol.replace(':', '')) === 'https';
45
+ response.cookies.set({ name: LOCALE_COOKIE, value: written.value, domain, path: '/', maxAge: LOCALE_COOKIE_MAX_AGE, sameSite: 'lax', secure });
46
+ response.headers.append('set-cookie', expireHostOnlyLocaleCookie(secure));
47
+ return response;
48
+ }
49
+
32
50
  /**
33
51
  * Create i18n proxy handler with custom routing configuration
34
52
  */
35
53
  export function createProxy(routingConfig?: ReturnType<typeof createRouting>) {
36
54
  const config = routingConfig ?? routing;
37
- return createIntlMiddleware(config);
55
+ const intl = createIntlMiddleware(config);
56
+ return (request: NextRequest) => shareLocaleCookie(request, intl(request) as NextResponse);
38
57
  }
39
58
 
40
59
  /**
41
60
  * Create i18n proxy from config options
42
61
  */
43
62
  export function createProxyFromConfig(config: Partial<I18nConfig>) {
44
- const routingConfig = createRouting(config);
45
- return createIntlMiddleware(routingConfig);
63
+ return createProxy(createRouting(config));
46
64
  }
47
65
 
48
66
  // ─────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,34 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+
4
+ import { expireHostOnlyLocaleCookie, localeCookieHeader, sharedCookieDomain } from './shared-locale';
5
+
6
+ test('the registrable domain of a subdomain host', () => {
7
+ assert.equal(sharedCookieDomain('hub.example.com'), 'example.com');
8
+ assert.equal(sharedCookieDomain('team.hub.example.com:3000'), 'example.com');
9
+ assert.equal(sharedCookieDomain('example.com'), 'example.com');
10
+ });
11
+
12
+ test('two-label public suffixes keep three labels', () => {
13
+ assert.equal(sharedCookieDomain('hub.example.co.uk'), 'example.co.uk');
14
+ });
15
+
16
+ test('hosts that cannot share get no domain', () => {
17
+ assert.equal(sharedCookieDomain('localhost:3211'), null);
18
+ assert.equal(sharedCookieDomain('127.0.0.1'), null);
19
+ assert.equal(sharedCookieDomain('[::1]'), null);
20
+ });
21
+
22
+ test('the override wins', () => {
23
+ assert.equal(sharedCookieDomain('hub.example.org', '.corp.example.org'), 'corp.example.org');
24
+ assert.equal(sharedCookieDomain('localhost', 'dev.test'), 'dev.test');
25
+ });
26
+
27
+ test('cookie headers', () => {
28
+ assert.equal(
29
+ localeCookieHeader('ru', 'example.com', true),
30
+ 'NEXT_LOCALE=ru; Path=/; Max-Age=31536000; SameSite=Lax; Domain=example.com; Secure',
31
+ );
32
+ assert.equal(localeCookieHeader('ru', null, false), 'NEXT_LOCALE=ru; Path=/; Max-Age=31536000; SameSite=Lax');
33
+ assert.equal(expireHostOnlyLocaleCookie(false), 'NEXT_LOCALE=; Path=/; Max-Age=0; SameSite=Lax');
34
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * One locale across every host of one product family.
3
+ *
4
+ * `NEXT_LOCALE` is written host-only by default, so a reader who picked Russian on `hub.example.com` meets English on
5
+ * `team.example.com`. The session tokens already live on the registrable domain (`@djangocfg/api` `createStorage`,
6
+ * `cookieDomain: 'auto'`); the locale follows the same rule: the cookie gets `Domain=` the registrable domain, and the
7
+ * host-only twin is expired so it cannot shadow the shared one.
8
+ *
9
+ * Pure and framework-free, so the proxy (server) and `useChangeLocale` (browser) agree on the domain.
10
+ */
11
+
12
+ export const LOCALE_COOKIE = 'NEXT_LOCALE';
13
+ export const LOCALE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
14
+
15
+ // Second-level public suffixes whose registrable domain has three labels. Not the whole Public Suffix List: the common
16
+ // ones; anything else sets NEXT_PUBLIC_COOKIE_DOMAIN.
17
+ const TWO_LABEL_SUFFIXES = new Set([
18
+ 'co.uk', 'org.uk', 'ac.uk', 'com.au', 'net.au', 'co.nz', 'co.jp', 'co.kr', 'or.kr', 'com.br', 'com.tr', 'com.cn', 'com.mx',
19
+ 'co.in', 'co.za', 'com.sg', 'com.hk', 'com.tw', 'com.ar', 'com.ua', 'co.il',
20
+ ]);
21
+
22
+ /**
23
+ * The domain a cookie must carry to be sent to every subdomain, or `null` for a host that cannot share (an IP, a bare
24
+ * label such as `localhost`, or an apex with no subdomain in use: a host-only cookie already covers it).
25
+ * `override` (`NEXT_PUBLIC_COOKIE_DOMAIN`) wins when set.
26
+ */
27
+ export function sharedCookieDomain(hostname: string, override?: string | null): string | null {
28
+ const forced = override?.trim().replace(/^\./, '');
29
+ if (forced) return forced;
30
+ const host = hostname.split(':')[0]!.toLowerCase();
31
+ if (!host || /^\d+(\.\d+){3}$/.test(host) || host.includes(':')) return null;
32
+ const labels = host.split('.');
33
+ if (labels.length < 2) return null;
34
+ const keep = TWO_LABEL_SUFFIXES.has(labels.slice(-2).join('.')) ? 3 : 2;
35
+ return labels.slice(-keep).join('.');
36
+ }
37
+
38
+ /** The `Set-Cookie` value that writes the shared locale cookie. */
39
+ export function localeCookieHeader(locale: string, domain: string | null, secure: boolean): string {
40
+ return `${LOCALE_COOKIE}=${encodeURIComponent(locale)}; Path=/; Max-Age=${LOCALE_COOKIE_MAX_AGE}; SameSite=Lax${domain ? `; Domain=${domain}` : ''}${secure ? '; Secure' : ''}`;
41
+ }
42
+
43
+ /** Expires the host-only twin of the cookie (a cookie is identified by name and Domain). */
44
+ export function expireHostOnlyLocaleCookie(secure: boolean): string {
45
+ return `${LOCALE_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax${secure ? '; Secure' : ''}`;
46
+ }