cloudflare-next-intl 0.8.40 → 0.8.41
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 +4 -3
- package/dist/src/cookie_consent/gdpr_countries.d.ts +1 -1
- package/dist/src/cookie_consent/gdpr_countries.js +14 -4
- package/dist/src/server/components/server_provider.js +1 -1
- package/dist/src/server/components/server_provider_static.js +1 -1
- package/dist/src/server/functions/geo.d.ts +10 -4
- package/dist/src/server/functions/geo.js +44 -10
- package/dist/src/types/types.d.ts +24 -6
- package/llms.txt +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -389,9 +389,10 @@ export default setIntlConfig({
|
|
|
389
389
|
privacyPolicyDate: "2026-01-01",
|
|
390
390
|
// privacyPolicyPath: "/privacy-policy", // default; used by the
|
|
391
391
|
// dialogs' auto-rendered link. Set false to disable that link.
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
392
|
+
// country-based gating is enabled by default (reads Cloudflare geo
|
|
393
|
+
// headers; override via countryHeaderNames). Visitors outside GDPR
|
|
394
|
+
// regions skip the banner and get consent immediately.
|
|
395
|
+
// countryHeaderNames: ["x-cf-country", "cf-ipcountry"],
|
|
395
396
|
// gdprCountries: [...], // defaults to EU/EEA + UK + Switzerland
|
|
396
397
|
// enableAnalyticsInDevMode: true, // analytics stay off in dev otherwise
|
|
397
398
|
// autoWireDialogs: false, // opt out and render the dialogs yourself
|
|
@@ -5,4 +5,4 @@ import type { CookieConsentGetCloudflareContext, ErrorHandlingRoutingConfig } fr
|
|
|
5
5
|
* (nFADP). ISO 3166-1 alpha-2.
|
|
6
6
|
*/
|
|
7
7
|
export declare const defaultGdprCountries: readonly string[];
|
|
8
|
-
export default function resolveRequiresConsent(getCountryCode: (() => string | undefined | Promise<string | undefined>) | undefined, getCloudflareContext: CookieConsentGetCloudflareContext | undefined, gdprCountries: readonly string[] | undefined, errorHandlingConfig?: ErrorHandlingRoutingConfig): Promise<boolean>;
|
|
8
|
+
export default function resolveRequiresConsent(getCountryCode: (() => string | undefined | Promise<string | undefined>) | undefined, getCloudflareContext: CookieConsentGetCloudflareContext | undefined, gdprCountries: readonly string[] | undefined, errorHandlingConfig?: ErrorHandlingRoutingConfig, countryHeaderNames?: readonly string[]): Promise<boolean>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import reportError from '../error_handling/report_error';
|
|
2
|
+
import { getCountry } from '../server/functions/geo';
|
|
2
3
|
/**
|
|
3
4
|
* Default `cookieConsent.gdprCountries` — EU/EEA member states (GDPR),
|
|
4
5
|
* Iceland/Liechtenstein/Norway (EEA), the UK (UK-GDPR), and Switzerland
|
|
@@ -31,14 +32,12 @@ function getGdprCountriesSet(gdprCountries) {
|
|
|
31
32
|
}
|
|
32
33
|
return set;
|
|
33
34
|
}
|
|
34
|
-
export default async function resolveRequiresConsent(getCountryCode, getCloudflareContext, gdprCountries, errorHandlingConfig) {
|
|
35
|
-
if (!getCountryCode && !getCloudflareContext)
|
|
36
|
-
return true;
|
|
35
|
+
export default async function resolveRequiresConsent(getCountryCode, getCloudflareContext, gdprCountries, errorHandlingConfig, countryHeaderNames) {
|
|
37
36
|
let countryCode;
|
|
38
37
|
if (getCountryCode) {
|
|
39
38
|
countryCode = await getCountryCode();
|
|
40
39
|
}
|
|
41
|
-
else {
|
|
40
|
+
else if (getCloudflareContext) {
|
|
42
41
|
try {
|
|
43
42
|
countryCode = (await getCloudflareContext({ async: true }))?.cf?.country;
|
|
44
43
|
}
|
|
@@ -47,6 +46,17 @@ export default async function resolveRequiresConsent(getCountryCode, getCloudfla
|
|
|
47
46
|
return true;
|
|
48
47
|
}
|
|
49
48
|
}
|
|
49
|
+
// Neither getter supplied (or one resolved nothing): fall back to the
|
|
50
|
+
// package's own geo resolution, which reads the Cloudflare country
|
|
51
|
+
// headers off the current request.
|
|
52
|
+
if (typeof countryCode !== 'string' || !countryCode) {
|
|
53
|
+
try {
|
|
54
|
+
countryCode = await getCountry(undefined, undefined, countryHeaderNames);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
50
60
|
if (typeof countryCode !== 'string' || !countryCode)
|
|
51
61
|
return true;
|
|
52
62
|
return getGdprCountriesSet(gdprCountries).has(countryCode);
|
|
@@ -144,7 +144,7 @@ export default async function LocationzationProvider({ language, messages, stati
|
|
|
144
144
|
// `getCloudflareContext` path in dev; fail-safe to `true`
|
|
145
145
|
// (banner shown) same as an unresolved country would.
|
|
146
146
|
requiresConsent = !isDevEnvironment
|
|
147
|
-
? await resolveRequiresConsent(config.cookieConsent.getCountryCode, config.generate?.getCloudflareContext, config.cookieConsent.gdprCountries, config.errorHandling)
|
|
147
|
+
? await resolveRequiresConsent(config.cookieConsent.getCountryCode, config.generate?.getCloudflareContext, config.cookieConsent.gdprCountries, config.errorHandling, config.cookieConsent.countryHeaderNames)
|
|
148
148
|
: false;
|
|
149
149
|
const analyticsAllowedInEnv = config.cookieConsent.enableAnalyticsInDevMode === true || !isDevEnvironment;
|
|
150
150
|
if (config.cookieConsent.autoWireAnalytics !== false && analyticsAllowedInEnv) {
|
|
@@ -87,7 +87,7 @@ export default async function LocationzationProvider({ language, messages, child
|
|
|
87
87
|
// `getCloudflareContext` path in dev; fail-safe to `true`
|
|
88
88
|
// (banner shown) same as an unresolved country would.
|
|
89
89
|
requiresConsent = !isDevEnvironment
|
|
90
|
-
? await resolveRequiresConsent(config.cookieConsent.getCountryCode, config.generate?.getCloudflareContext, config.cookieConsent.gdprCountries, config.errorHandling)
|
|
90
|
+
? await resolveRequiresConsent(config.cookieConsent.getCountryCode, config.generate?.getCloudflareContext, config.cookieConsent.gdprCountries, config.errorHandling, config.cookieConsent.countryHeaderNames)
|
|
91
91
|
: false;
|
|
92
92
|
const analyticsAllowedInEnv = config.cookieConsent.enableAnalyticsInDevMode === true || !isDevEnvironment;
|
|
93
93
|
if (config.cookieConsent.autoWireAnalytics !== false && analyticsAllowedInEnv) {
|
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import type { GenerateRoutingConfig, RequestOrHeaders } from '../../types/types';
|
|
2
|
+
/** Default request headers read to resolve the visitor's country, in order. */
|
|
3
|
+
export declare const defaultCountryHeaderNames: readonly string[];
|
|
4
|
+
/** Default request headers read to resolve the visitor's timezone, in order. */
|
|
5
|
+
export declare const defaultTimezoneHeaderNames: readonly string[];
|
|
2
6
|
/**
|
|
3
7
|
* Resolves the client's ISO 3166-1 alpha-2 country code (e.g. "US", "DE", "UA").
|
|
4
8
|
*
|
|
5
9
|
* Checks in order:
|
|
6
10
|
* 1. Explicit `input` (Request, NextRequest, or Headers) if provided
|
|
7
|
-
* 2. Next.js request headers via `headers()` (`
|
|
11
|
+
* 2. Next.js request headers via `headers()` (`headerNames`, default
|
|
12
|
+
* `x-cf-country`, `cf-ipcountry`)
|
|
8
13
|
* 3. `generate.getCloudflareContext` or `cf.country` if passed
|
|
9
14
|
* 4. `undefined` if outside request scope or unavailable
|
|
10
15
|
*/
|
|
11
|
-
export declare function getCountry(input?: RequestOrHeaders, generate?: GenerateRoutingConfig): Promise<string | undefined>;
|
|
16
|
+
export declare function getCountry(input?: RequestOrHeaders, generate?: GenerateRoutingConfig, headerNames?: readonly string[]): Promise<string | undefined>;
|
|
12
17
|
/**
|
|
13
18
|
* Resolves the client's IANA timezone string (e.g. "America/New_York", "Europe/Kyiv", "UTC").
|
|
14
19
|
*
|
|
15
20
|
* Checks in order:
|
|
16
21
|
* 1. Explicit `input` (Request, NextRequest, or Headers) if provided
|
|
17
|
-
* 2. Next.js request headers via `headers()` (`
|
|
22
|
+
* 2. Next.js request headers via `headers()` (`headerNames`, default
|
|
23
|
+
* `x-cf-timezone`, `cf-timezone`)
|
|
18
24
|
* 3. `generate.getCloudflareContext` or `cf.timezone` if passed
|
|
19
25
|
* 4. `fallback` (or `undefined`) if outside request scope or unavailable
|
|
20
26
|
*/
|
|
21
|
-
export declare function getTimezone(input?: RequestOrHeaders, fallback?: string, generate?: GenerateRoutingConfig): Promise<string | undefined>;
|
|
27
|
+
export declare function getTimezone(input?: RequestOrHeaders, fallback?: string, generate?: GenerateRoutingConfig, headerNames?: readonly string[]): Promise<string | undefined>;
|
|
22
28
|
/**
|
|
23
29
|
* Resolves the Cloudflare environment bindings object from `generate.env` or `generate.getCloudflareContext`.
|
|
24
30
|
*/
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** Default request headers read to resolve the visitor's country, in order. */
|
|
2
|
+
export const defaultCountryHeaderNames = ['x-cf-country', 'cf-ipcountry'];
|
|
3
|
+
/** Default request headers read to resolve the visitor's timezone, in order. */
|
|
4
|
+
export const defaultTimezoneHeaderNames = ['x-cf-timezone', 'cf-timezone'];
|
|
1
5
|
function extractHeader(h, name) {
|
|
2
6
|
if (typeof h.get === 'function') {
|
|
3
7
|
const val = h.get(name);
|
|
@@ -7,24 +11,49 @@ function extractHeader(h, name) {
|
|
|
7
11
|
const val = rec[name] ?? rec[name.toLowerCase()];
|
|
8
12
|
return (typeof val === 'string' && val.length > 0) ? val : undefined;
|
|
9
13
|
}
|
|
14
|
+
// Read lazily (and tolerantly): `@intl-config` may not be set at all in
|
|
15
|
+
// standalone/unit usage of these helpers, and importing the config eagerly
|
|
16
|
+
// would risk a cycle with a config module that itself imports from here.
|
|
17
|
+
async function configuredHeaderNames(key) {
|
|
18
|
+
try {
|
|
19
|
+
const config = (await import('../../config/intl_config')).default;
|
|
20
|
+
return config?.generate?.[key];
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function extractFromHeaderNames(h, headerNames) {
|
|
27
|
+
for (const name of headerNames) {
|
|
28
|
+
const val = extractHeader(h, name);
|
|
29
|
+
if (val)
|
|
30
|
+
return val;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
10
34
|
/**
|
|
11
35
|
* Resolves the client's ISO 3166-1 alpha-2 country code (e.g. "US", "DE", "UA").
|
|
12
36
|
*
|
|
13
37
|
* Checks in order:
|
|
14
38
|
* 1. Explicit `input` (Request, NextRequest, or Headers) if provided
|
|
15
|
-
* 2. Next.js request headers via `headers()` (`
|
|
39
|
+
* 2. Next.js request headers via `headers()` (`headerNames`, default
|
|
40
|
+
* `x-cf-country`, `cf-ipcountry`)
|
|
16
41
|
* 3. `generate.getCloudflareContext` or `cf.country` if passed
|
|
17
42
|
* 4. `undefined` if outside request scope or unavailable
|
|
18
43
|
*/
|
|
19
|
-
export async function getCountry(input, generate) {
|
|
44
|
+
export async function getCountry(input, generate, headerNames) {
|
|
45
|
+
const names = headerNames
|
|
46
|
+
?? generate?.countryHeaderNames
|
|
47
|
+
?? await configuredHeaderNames('countryHeaderNames')
|
|
48
|
+
?? defaultCountryHeaderNames;
|
|
20
49
|
if (input) {
|
|
21
50
|
if ('headers' in input && input.headers) {
|
|
22
|
-
const country =
|
|
51
|
+
const country = extractFromHeaderNames(input.headers, names);
|
|
23
52
|
if (country)
|
|
24
53
|
return country;
|
|
25
54
|
}
|
|
26
55
|
else if (typeof input.get === 'function') {
|
|
27
|
-
const country = input
|
|
56
|
+
const country = extractFromHeaderNames(input, names);
|
|
28
57
|
if (country)
|
|
29
58
|
return country;
|
|
30
59
|
}
|
|
@@ -36,7 +65,7 @@ export async function getCountry(input, generate) {
|
|
|
36
65
|
try {
|
|
37
66
|
const { headers } = await import('next/headers');
|
|
38
67
|
const h = await headers();
|
|
39
|
-
const country = h
|
|
68
|
+
const country = extractFromHeaderNames(h, names);
|
|
40
69
|
if (country)
|
|
41
70
|
return country;
|
|
42
71
|
}
|
|
@@ -61,19 +90,24 @@ export async function getCountry(input, generate) {
|
|
|
61
90
|
*
|
|
62
91
|
* Checks in order:
|
|
63
92
|
* 1. Explicit `input` (Request, NextRequest, or Headers) if provided
|
|
64
|
-
* 2. Next.js request headers via `headers()` (`
|
|
93
|
+
* 2. Next.js request headers via `headers()` (`headerNames`, default
|
|
94
|
+
* `x-cf-timezone`, `cf-timezone`)
|
|
65
95
|
* 3. `generate.getCloudflareContext` or `cf.timezone` if passed
|
|
66
96
|
* 4. `fallback` (or `undefined`) if outside request scope or unavailable
|
|
67
97
|
*/
|
|
68
|
-
export async function getTimezone(input, fallback, generate) {
|
|
98
|
+
export async function getTimezone(input, fallback, generate, headerNames) {
|
|
99
|
+
const names = headerNames
|
|
100
|
+
?? generate?.timezoneHeaderNames
|
|
101
|
+
?? await configuredHeaderNames('timezoneHeaderNames')
|
|
102
|
+
?? defaultTimezoneHeaderNames;
|
|
69
103
|
if (input) {
|
|
70
104
|
if ('headers' in input && input.headers) {
|
|
71
|
-
const tz =
|
|
105
|
+
const tz = extractFromHeaderNames(input.headers, names);
|
|
72
106
|
if (tz)
|
|
73
107
|
return tz;
|
|
74
108
|
}
|
|
75
109
|
else if (typeof input.get === 'function') {
|
|
76
|
-
const tz = input
|
|
110
|
+
const tz = extractFromHeaderNames(input, names);
|
|
77
111
|
if (tz)
|
|
78
112
|
return tz;
|
|
79
113
|
}
|
|
@@ -85,7 +119,7 @@ export async function getTimezone(input, fallback, generate) {
|
|
|
85
119
|
try {
|
|
86
120
|
const { headers } = await import('next/headers');
|
|
87
121
|
const h = await headers();
|
|
88
|
-
const tz = h
|
|
122
|
+
const tz = extractFromHeaderNames(h, names);
|
|
89
123
|
if (tz)
|
|
90
124
|
return tz;
|
|
91
125
|
}
|
|
@@ -144,6 +144,19 @@ export interface GenerateRoutingConfig {
|
|
|
144
144
|
* Set one of the two getters to scope the banner to GDPR regions only.
|
|
145
145
|
*/
|
|
146
146
|
getCloudflareContext?: CookieConsentGetCloudflareContext;
|
|
147
|
+
/**
|
|
148
|
+
* Request headers read (in order) by `getCountry()` to resolve the
|
|
149
|
+
* visitor's country. Defaults to `['x-cf-country', 'cf-ipcountry']` —
|
|
150
|
+
* override when your edge/proxy forwards it under a different name.
|
|
151
|
+
* `cookieConsent.countryHeaderNames` takes precedence for the
|
|
152
|
+
* cookie-consent banner's own resolution.
|
|
153
|
+
*/
|
|
154
|
+
countryHeaderNames?: readonly string[];
|
|
155
|
+
/**
|
|
156
|
+
* Request headers read (in order) by `getTimezone()` to resolve the
|
|
157
|
+
* visitor's IANA timezone. Defaults to `['x-cf-timezone', 'cf-timezone']`.
|
|
158
|
+
*/
|
|
159
|
+
timezoneHeaderNames?: readonly string[];
|
|
147
160
|
}
|
|
148
161
|
/**
|
|
149
162
|
* Flexible input accepted by `getCountry()` and `getTimezone()`:
|
|
@@ -359,14 +372,19 @@ export interface CookieConsentRoutingConfig {
|
|
|
359
372
|
* over `getCloudflareContext` when both are set.
|
|
360
373
|
*/
|
|
361
374
|
getCountryCode?: () => string | undefined | Promise<string | undefined>;
|
|
375
|
+
/**
|
|
376
|
+
* Request headers read (in order) to resolve the visitor's country when
|
|
377
|
+
* neither `getCountryCode` nor `generate.getCloudflareContext` produced
|
|
378
|
+
* one. Defaults to `['x-cf-country', 'cf-ipcountry']` — override when
|
|
379
|
+
* your edge/proxy forwards the country under a different name.
|
|
380
|
+
*/
|
|
381
|
+
countryHeaderNames?: readonly string[];
|
|
362
382
|
/**
|
|
363
383
|
* Country codes (ISO 3166-1 alpha-2) for which the cookie-consent banner
|
|
364
|
-
* is required.
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* country that couldn't be resolved still requires it (fail-safe:
|
|
369
|
-
* unknown defaults to "ask").
|
|
384
|
+
* is required. Defaults to the EU/EEA + UK + Switzerland
|
|
385
|
+
* (GDPR/UK-GDPR/nFADP scope). A visitor whose resolved country isn't in
|
|
386
|
+
* this set is treated as NOT requiring consent; a country that couldn't
|
|
387
|
+
* be resolved still requires it (fail-safe: unknown defaults to "ask").
|
|
370
388
|
*/
|
|
371
389
|
gdprCountries?: readonly string[];
|
|
372
390
|
/**
|
package/llms.txt
CHANGED
|
@@ -11,7 +11,7 @@ other subpath can be used.
|
|
|
11
11
|
- `.` — everything, re-exported (prefer the flat subpaths below for smaller bundles).
|
|
12
12
|
- `./client` — client-side barrel: `LocaleLink`, `usePathname`, `setCookieClient`, `getCookieClient`.
|
|
13
13
|
- `./server` — server-side barrel: `getMessage`, `getTranslations`, `getLocale`, `getCountry`, `getTimezone`, `resolveEnv`, `IntlProvider`, `Link`, `IntlHelperScript`, `getLocaleStaticParams`.
|
|
14
|
-
- `./geo` / `./getCountry` / `./getTimezone` — country & timezone resolver helpers (`getCountry(input?)`, `getTimezone(input?, fallback?)`, `resolveEnv(generate?)`) for Vinext, OpenNext, and Cloudflare Workers.
|
|
14
|
+
- `./geo` / `./getCountry` / `./getTimezone` — country & timezone resolver helpers (`getCountry(input?, generate?, headerNames?)`, `getTimezone(input?, fallback?, generate?, headerNames?)`, `resolveEnv(generate?)`) for Vinext, OpenNext, and Cloudflare Workers. Custom header names can be configured via `generate.countryHeaderNames`/`generate.timezoneHeaderNames` or passed per-call.
|
|
15
15
|
- `./middleware` — `intlMiddleware` for `middleware.ts`; locale detection/rewrite/redirect and automatic CF header forwarding.
|
|
16
16
|
- `./setIntlConfig` — identity function for typed `RoutingConfig` authoring; use in your `@intl-config` file.
|
|
17
17
|
- `./serverProvider` — `IntlProvider` server component (also under `./server`).
|
|
@@ -46,7 +46,7 @@ other subpath can be used.
|
|
|
46
46
|
- `./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.
|
|
47
47
|
- `./PrivacyPolicyUpdateDialog` — "privacy policy updated" banner; auto-enabled only when `cookieConsent.privacyPolicyDate` is set. Same default-link behavior as `CookieConsentDialog` (label default `"Learn more"`).
|
|
48
48
|
- `./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`.
|
|
49
|
-
- Country-based gating (`cookieConsent.getCountryCode` / `getCloudflareContext` + `gdprCountries`): resolved server-side by `IntlProvider` into a `requiresConsent` boolean passed to `CookieConsentProvider`.
|
|
49
|
+
- Country-based gating (`cookieConsent.countryHeaderNames` / `getCountryCode` / `getCloudflareContext` + `gdprCountries`): resolved server-side by `IntlProvider` into a `requiresConsent` boolean passed to `CookieConsentProvider`. Automatically falls back to request headers (`x-cf-country`, `cf-ipcountry` or custom `countryHeaderNames`) when neither getter is set or when they resolve empty. `getCountryCode` (direct country resolver) takes precedence over `getCloudflareContext` (reads `cf.country`) when both are set. Unresolved country (or a `null` context) always requires consent (fail-safe). `./cookieConsent` also exports `defaultGdprCountries` (EU/EEA + UK + Switzerland).
|
|
50
50
|
|
|
51
51
|
## `db*` subpaths (require `db` set on your `RoutingConfig`)
|
|
52
52
|
|