cloudflare-next-intl 0.4.2 → 0.4.4

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
@@ -200,6 +200,8 @@ export default setIntlConfig({
200
200
  defaultLocale: "en",
201
201
  cookieConsent: {
202
202
  privacyPolicyDate: "2026-01-01",
203
+ // privacyPolicyPath: "/privacy-policy", // default; used by the
204
+ // dialogs' auto-rendered link. Set false to disable that link.
203
205
  // Optional: gate the banner to GDPR-region visitors only. Omit both
204
206
  // getters to disable country-based gating (consent always implicit).
205
207
  // Pass @opennextjs/cloudflare's getCloudflareContext directly — its
@@ -2,8 +2,15 @@ import type { CookieDialogClassNames, CookieDialogStyles } from '../../types';
2
2
  export interface CookieConsentDialogProps {
3
3
  /** Banner message text. */
4
4
  message?: React.ReactNode;
5
- /** Optional link element rendered right after `message` (e.g. a privacy-policy link). */
5
+ /**
6
+ * Link element rendered right after `message`. Defaults to a link to
7
+ * `cookieConsent.privacyPolicyPath` (`'/privacy-policy'` unless
8
+ * configured otherwise) with `privacyPolicyLinkText` as its label. Pass
9
+ * `null` to render no link, or your own element to override it.
10
+ */
6
11
  link?: React.ReactNode;
12
+ /** Label for the default privacy-policy link. Ignored when `link` is set. */
13
+ privacyPolicyLinkText?: string;
7
14
  acceptText?: string;
8
15
  declineText?: string;
9
16
  /** Hides the decline ("necessary only") button, leaving only accept. */
@@ -25,4 +32,4 @@ export interface CookieConsentDialogProps {
25
32
  * `render` (full custom markup) — none of it is hardcoded to Tailwind or any
26
33
  * particular design system.
27
34
  */
28
- export default function CookieConsentDialog({ message, link, acceptText, declineText, hideDecline, id, classNames, styles, render, }: CookieConsentDialogProps): React.ReactElement | null;
35
+ export default function CookieConsentDialog({ message, link, privacyPolicyLinkText, acceptText, declineText, hideDecline, id, classNames, styles, render, }: CookieConsentDialogProps): React.ReactElement | null;
@@ -1,17 +1,21 @@
1
1
  'use client';
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import useCookieConsent from '../use_cookie_consent';
4
+ import DefaultPrivacyPolicyLink from './default_privacy_policy_link';
4
5
  /**
5
6
  * Cookie-consent banner. Renders `null` once `consent` is already decided.
6
7
  * Every visual aspect is overridable via `classNames`/`styles` (per-slot) or
7
8
  * `render` (full custom markup) — none of it is hardcoded to Tailwind or any
8
9
  * particular design system.
9
10
  */
10
- export default function CookieConsentDialog({ message = 'We use cookies to improve your experience.', link, acceptText = 'Accept', declineText = 'Necessary only', hideDecline = false, id = 'cookie-consent-dialog', classNames, styles, render, }) {
11
- const { consent, setConsent } = useCookieConsent();
11
+ export default function CookieConsentDialog({ message = 'We use cookies to improve your experience.', link, privacyPolicyLinkText = 'Privacy Policy', acceptText = 'Accept', declineText = 'Necessary only', hideDecline = false, id = 'cookie-consent-dialog', classNames, styles, render, }) {
12
+ const { consent, setConsent, privacyPolicyPath } = useCookieConsent();
12
13
  if (consent !== null)
13
14
  return null;
14
15
  if (render)
15
16
  return _jsx(_Fragment, { children: render({ setConsent }) });
16
- return (_jsxs("div", { id: id, role: "dialog", "aria-modal": "false", "aria-labelledby": `${id}-title`, className: classNames?.root, style: styles?.root, children: [_jsxs("p", { id: `${id}-title`, className: classNames?.message, style: styles?.message, children: [message, link ? _jsxs("span", { className: classNames?.link, style: styles?.link, children: [" ", link] }) : null] }), _jsxs("div", { className: classNames?.actions, style: styles?.actions, children: [!hideDecline && (_jsx("button", { type: "button", onClick: () => setConsent(false), className: classNames?.declineButton, style: styles?.declineButton, children: declineText })), _jsx("button", { type: "button", onClick: () => setConsent(true), className: classNames?.acceptButton, style: styles?.acceptButton, children: acceptText })] })] }));
17
+ const resolvedLink = link !== undefined
18
+ ? link
19
+ : _jsx(DefaultPrivacyPolicyLink, { privacyPolicyPath: privacyPolicyPath, text: privacyPolicyLinkText });
20
+ return (_jsxs("div", { id: id, role: "dialog", "aria-modal": "false", "aria-labelledby": `${id}-title`, className: classNames?.root, style: styles?.root, children: [_jsxs("p", { id: `${id}-title`, className: classNames?.message, style: styles?.message, children: [message, resolvedLink ? _jsxs("span", { className: classNames?.link, style: styles?.link, children: [" ", resolvedLink] }) : null] }), _jsxs("div", { className: classNames?.actions, style: styles?.actions, children: [!hideDecline && (_jsx("button", { type: "button", onClick: () => setConsent(false), className: classNames?.declineButton, style: styles?.declineButton, children: declineText })), _jsx("button", { type: "button", onClick: () => setConsent(true), className: classNames?.acceptButton, style: styles?.acceptButton, children: acceptText })] })] }));
17
21
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Renders the default privacy-policy link used by `CookieConsentDialog`/
3
+ * `PrivacyPolicyUpdateDialog` when their `link` prop is omitted. Returns
4
+ * `null` when `privacyPolicyPath` is `false` (disabled via
5
+ * `cookieConsent.privacyPolicyPath`). Locale-prefixes `privacyPolicyPath`
6
+ * the same way the server `Link` component does — reads the locale set by
7
+ * `LocationzationClientProvider` on the current render.
8
+ */
9
+ export default function DefaultPrivacyPolicyLink({ privacyPolicyPath, text, className, style }: {
10
+ privacyPolicyPath: string | false;
11
+ text: string;
12
+ className?: string;
13
+ style?: React.CSSProperties;
14
+ }): React.ReactElement | null;
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import NextLink from 'next/link';
4
+ import config from '../../../config/intl_config';
5
+ import { getLocaleCache } from '../../../general/cache_variables';
6
+ /**
7
+ * Renders the default privacy-policy link used by `CookieConsentDialog`/
8
+ * `PrivacyPolicyUpdateDialog` when their `link` prop is omitted. Returns
9
+ * `null` when `privacyPolicyPath` is `false` (disabled via
10
+ * `cookieConsent.privacyPolicyPath`). Locale-prefixes `privacyPolicyPath`
11
+ * the same way the server `Link` component does — reads the locale set by
12
+ * `LocationzationClientProvider` on the current render.
13
+ */
14
+ export default function DefaultPrivacyPolicyLink({ privacyPolicyPath, text, className, style }) {
15
+ if (privacyPolicyPath === false)
16
+ return null;
17
+ const localeValue = getLocaleCache();
18
+ const needsLangPath = localeValue !== config.defaultLocale || !localeValue;
19
+ const href = needsLangPath ? `/${localeValue}${privacyPolicyPath}` : privacyPolicyPath;
20
+ return (_jsx(NextLink, { href: href, className: className, style: style, children: text }));
21
+ }
@@ -1,8 +1,15 @@
1
1
  import type { CookieDialogClassNames, CookieDialogStyles } from '../../types';
2
2
  export interface PrivacyPolicyUpdateDialogProps {
3
3
  message?: React.ReactNode;
4
- /** Optional link element rendered right after `message` (e.g. to your privacy-policy page). */
4
+ /**
5
+ * Link element rendered right after `message`. Defaults to a link to
6
+ * `cookieConsent.privacyPolicyPath` (`'/privacy-policy'` unless
7
+ * configured otherwise) with `privacyPolicyLinkText` as its label. Pass
8
+ * `null` to render no link, or your own element to override it.
9
+ */
5
10
  link?: React.ReactNode;
11
+ /** Label for the default privacy-policy link. Ignored when `link` is set. */
12
+ privacyPolicyLinkText?: string;
6
13
  closeText?: string;
7
14
  id?: string;
8
15
  classNames?: CookieDialogClassNames;
@@ -21,4 +28,4 @@ export interface PrivacyPolicyUpdateDialogProps {
21
28
  * `null` otherwise, or once acknowledged. Every visual aspect is overridable
22
29
  * via `classNames`/`styles` (per-slot) or `render` (full custom markup).
23
30
  */
24
- export default function PrivacyPolicyUpdateDialog({ message, link, closeText, id, classNames, styles, render, }: PrivacyPolicyUpdateDialogProps): React.ReactElement | null;
31
+ export default function PrivacyPolicyUpdateDialog({ message, link, privacyPolicyLinkText, closeText, id, classNames, styles, render, }: PrivacyPolicyUpdateDialogProps): React.ReactElement | null;
@@ -1,17 +1,21 @@
1
1
  'use client';
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import useCookieConsent from '../use_cookie_consent';
4
+ import DefaultPrivacyPolicyLink from './default_privacy_policy_link';
4
5
  /**
5
6
  * "Privacy policy updated" banner. Auto-enabled only when
6
7
  * `cookieConsent.privacyPolicyDate` is set on the `RoutingConfig` — renders
7
8
  * `null` otherwise, or once acknowledged. Every visual aspect is overridable
8
9
  * via `classNames`/`styles` (per-slot) or `render` (full custom markup).
9
10
  */
10
- export default function PrivacyPolicyUpdateDialog({ message = 'Our privacy policy has been updated.', link, closeText = 'Got it', id = 'privacy-policy-update-dialog', classNames, styles, render, }) {
11
- const { privacyPolicyUpdated, acknowledgePrivacyPolicyUpdate } = useCookieConsent();
11
+ export default function PrivacyPolicyUpdateDialog({ message = 'Our privacy policy has been updated.', link, privacyPolicyLinkText = 'Learn more', closeText = 'Got it', id = 'privacy-policy-update-dialog', classNames, styles, render, }) {
12
+ const { privacyPolicyUpdated, acknowledgePrivacyPolicyUpdate, privacyPolicyPath } = useCookieConsent();
12
13
  if (!privacyPolicyUpdated)
13
14
  return null;
14
15
  if (render)
15
16
  return _jsx(_Fragment, { children: render({ acknowledge: acknowledgePrivacyPolicyUpdate }) });
16
- return (_jsxs("div", { id: id, role: "dialog", "aria-modal": "false", "aria-labelledby": `${id}-title`, className: classNames?.root, style: styles?.root, children: [_jsxs("p", { id: `${id}-title`, className: classNames?.message, style: styles?.message, children: [message, link ? _jsxs("span", { className: classNames?.link, style: styles?.link, children: [" ", link] }) : null] }), _jsx("button", { type: "button", onClick: acknowledgePrivacyPolicyUpdate, "aria-label": closeText, className: classNames?.closeButton, style: styles?.closeButton, children: closeText })] }));
17
+ const resolvedLink = link !== undefined
18
+ ? link
19
+ : _jsx(DefaultPrivacyPolicyLink, { privacyPolicyPath: privacyPolicyPath, text: privacyPolicyLinkText });
20
+ return (_jsxs("div", { id: id, role: "dialog", "aria-modal": "false", "aria-labelledby": `${id}-title`, className: classNames?.root, style: styles?.root, children: [_jsxs("p", { id: `${id}-title`, className: classNames?.message, style: styles?.message, children: [message, resolvedLink ? _jsxs("span", { className: classNames?.link, style: styles?.link, children: [" ", resolvedLink] }) : null] }), _jsx("button", { type: "button", onClick: acknowledgePrivacyPolicyUpdate, "aria-label": closeText, className: classNames?.closeButton, style: styles?.closeButton, children: closeText })] }));
17
21
  }
@@ -45,13 +45,14 @@ function parseConsent(raw) {
45
45
  * ```
46
46
  */
47
47
  export default function CookieConsentProvider({ requiresConsent = true, children }) {
48
- const { consentCookieName, dateCookieName, maxAge, policyDate } = useMemo(() => {
48
+ const { consentCookieName, dateCookieName, maxAge, policyDate, privacyPolicyPath } = useMemo(() => {
49
49
  const cc = requireCookieConsentConfig(config.cookieConsent);
50
50
  return {
51
51
  consentCookieName: cc.consentCookieName ?? cookieConsentCookieKey,
52
52
  dateCookieName: cc.privacyPolicyDateCookieName ?? privacyPolicyDateCookieKey,
53
53
  maxAge: cc.cookieMaxAge ?? 31536000,
54
54
  policyDate: cc.privacyPolicyDate ? new Date(cc.privacyPolicyDate) : null,
55
+ privacyPolicyPath: cc.privacyPolicyPath ?? '/privacy-policy',
55
56
  };
56
57
  // eslint-disable-next-line react-hooks/exhaustive-deps
57
58
  }, []);
@@ -89,6 +90,6 @@ export default function CookieConsentProvider({ requiresConsent = true, children
89
90
  setPrivacyPolicyUpdated(false);
90
91
  // eslint-disable-next-line react-hooks/exhaustive-deps
91
92
  }, []);
92
- const contextValue = useMemo(() => ({ consent, privacyPolicyUpdated, setConsent, acknowledgePrivacyPolicyUpdate }), [consent, privacyPolicyUpdated, setConsent, acknowledgePrivacyPolicyUpdate]);
93
+ const contextValue = useMemo(() => ({ consent, privacyPolicyUpdated, setConsent, acknowledgePrivacyPolicyUpdate, privacyPolicyPath }), [consent, privacyPolicyUpdated, setConsent, acknowledgePrivacyPolicyUpdate, privacyPolicyPath]);
93
94
  return (_jsx(CookieConsentContext.Provider, { value: contextValue, children: children }));
94
95
  }
@@ -46,7 +46,7 @@ export default async function resolveRequiresConsent(getCountryCode, getCloudfla
46
46
  const countryCode = getCountryCode
47
47
  ? await getCountryCode()
48
48
  : (await getCloudflareContext({ async: true }))?.cf?.country;
49
- if (!countryCode)
49
+ if (typeof countryCode !== 'string' || !countryCode)
50
50
  return true;
51
51
  return getGdprCountriesSet(gdprCountries).has(countryCode);
52
52
  }
@@ -14,6 +14,13 @@ export interface CookieConsentContextType {
14
14
  setConsent: (value: boolean) => void;
15
15
  /** Acknowledges the privacy-policy update banner and persists the new date. */
16
16
  acknowledgePrivacyPolicyUpdate: () => void;
17
+ /**
18
+ * Resolved from `cookieConsent.privacyPolicyPath` (defaults to
19
+ * `'/privacy-policy'`; `false` disables it). Used by the default
20
+ * dialog components to render a privacy-policy link automatically
21
+ * when their `link` prop is omitted.
22
+ */
23
+ privacyPolicyPath: string | false;
17
24
  }
18
25
  /** Slot-level style/class overrides accepted by the default dialog components. */
19
26
  export interface CookieDialogClassNames {
@@ -98,6 +98,14 @@ export interface CookieConsentRoutingConfig {
98
98
  * cookie-consent banner still works independently).
99
99
  */
100
100
  privacyPolicyDate?: string | Date;
101
+ /**
102
+ * Path to your privacy-policy page, e.g. `"/privacy-policy"`. Used by
103
+ * `CookieConsentDialog`/`PrivacyPolicyUpdateDialog` to render a default
104
+ * link automatically when their `link` prop is omitted. Defaults to
105
+ * `'/privacy-policy'`. Set `false` to render no link by default (still
106
+ * overridable per-dialog via the `link` prop).
107
+ */
108
+ privacyPolicyPath?: string | false;
101
109
  /** Cookie-consent cookie name. Defaults to `'__cookie_consent_key__'`. */
102
110
  consentCookieName?: string;
103
111
  /** Privacy-policy-date cookie name. Defaults to `'__privacy_policy_date_key__'`. */
@@ -175,14 +183,15 @@ export interface CookieConsentRoutingConfig {
175
183
  }
176
184
  /**
177
185
  * Minimal shape read from your `getCloudflareContext()` return value — only
178
- * `cf.country` is consulted, so any superset (the real `CloudflareContext`
179
- * from `@opennextjs/cloudflare`) is accepted as-is without a hard
186
+ * `cf.country` is consulted (read defensively at the call site, since `cf`'s
187
+ * real type — `@opennextjs/cloudflare`'s `CfProperties`, a union of the
188
+ * incoming-request and request-init variants — only has `country` on one
189
+ * branch). `cf` is typed loosely here so the real (generic) function is
190
+ * assignable to `CookieConsentGetCloudflareContext` without a hard
180
191
  * dependency on that package.
181
192
  */
182
193
  export interface CookieConsentCloudflareContext {
183
- cf?: {
184
- country?: string;
185
- };
194
+ cf?: Record<string, unknown>;
186
195
  }
187
196
  /**
188
197
  * Matches `@opennextjs/cloudflare`'s `getCloudflareContext` overloaded
package/llms.txt CHANGED
@@ -39,8 +39,8 @@ other subpath can be used.
39
39
  - `./cookieConsent` — barrel: `CookieConsentProvider`, `useCookieConsent`, `CookieConsentDialog`, `PrivacyPolicyUpdateDialog`, `CookieConsentAnalytics`.
40
40
  - `./CookieConsentProvider` — context provider; reads/writes consent + privacy-policy-date cookies. Auto-wired by `IntlProvider` when `cookieConsent` is configured — manual nesting is optional.
41
41
  - `./useCookieConsent` — context hook; throws `"useCookieConsent must be used within a CookieConsentProvider"` if called outside one.
42
- - `./CookieConsentDialog` — default cookie-consent banner; accepts per-slot `classNames`/`styles` or a `render` prop for fully custom markup.
43
- - `./PrivacyPolicyUpdateDialog` — "privacy policy updated" banner; auto-enabled only when `cookieConsent.privacyPolicyDate` is set.
42
+ - `./CookieConsentDialog` — default cookie-consent banner; accepts per-slot `classNames`/`styles` or a `render` prop for fully custom markup. When `link` is omitted, renders a default link to `cookieConsent.privacyPolicyPath` (defaults to `'/privacy-policy'`; label via `privacyPolicyLinkText`, default `"Privacy Policy"`) — pass `link={null}` for no link, or set `privacyPolicyPath: false` to disable it everywhere.
43
+ - `./PrivacyPolicyUpdateDialog` — "privacy policy updated" banner; auto-enabled only when `cookieConsent.privacyPolicyDate` is set. Same default-link behavior as `CookieConsentDialog` (label default `"Learn more"`).
44
44
  - `./cookieConsentAnalytics` — `CookieConsentAnalytics`: gates Cloudflare Web Analytics / Google Ads / Google Analytics / AdSense / Microsoft Clarity behind consent; rendered automatically by `IntlProvider` when `cookieConsent.secrets` or `getSecrets` is set (and `autoWireAnalytics !== false`). Never renders in local dev (`NODE_ENV === 'development'`) unless `cookieConsent.enableAnalyticsInDevMode` is `true`.
45
45
  - Country-based gating (`cookieConsent.getCountryCode` / `getCloudflareContext` + `gdprCountries`): resolved server-side by `IntlProvider` into a `requiresConsent` boolean passed to `CookieConsentProvider`. Neither getter set → gating off, consent always implicitly granted. `getCountryCode` (direct country resolver) takes precedence over `getCloudflareContext` (reads `cf.country`) when both are set. `getCloudflareContext` accepts `@opennextjs/cloudflare`'s `getCloudflareContext` function directly (its exact overloaded signature — `CookieConsentGetCloudflareContext`), called internally with `{ async: true }`. Unresolved country (or a `null` context) always requires consent (fail-safe). `./cookieConsent` also exports `defaultGdprCountries` (EU/EEA + UK + Switzerland).
46
46
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Optimized Next Intl Package Special for App Router and Cloudflare",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",