cloudflare-next-intl 0.13.1 → 0.13.3

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
@@ -599,6 +599,8 @@ export default setIntlConfig({
599
599
  // countryHeaderNames: ["x-cf-country", "cf-ipcountry"],
600
600
  // gdprCountries: [...], // defaults to EU/EEA + UK + Switzerland
601
601
  // enableAnalyticsInDevMode: true, // analytics stay off in dev otherwise
602
+ // googleTagId: "GT-XXXXXXXX", // gtag.js in IntlHelperScript; defaults to analytics.googleAdsId
603
+ // googleTag: false, // turn off the IntlHelperScript Google tag
602
604
  // autoWireDialogs: false, // opt out and render the dialogs yourself
603
605
  // dialogProps: { acceptText: "Accept", showPrivacyPolicy: true }, // forwarded to CookieConsentDialog
604
606
  // updateDialogProps: { closeText: "Got it", showPrivacyPolicy: true }, // forwarded to PrivacyPolicyUpdateDialog
@@ -31,7 +31,7 @@ export default function ClientExtensions({ initialAuthUser = null, skipAuthProvi
31
31
  providedChildren = (_jsxs(AuthUserProvider, { initialUser: initialAuthUser, children: [children, isPerf && (_jsx(Suspense, { fallback: null, children: _jsx(AutoFirebasePerformanceEvents, {}) }))] }));
32
32
  }
33
33
  if (isConsent) {
34
- providedChildren = (_jsxs(CookieConsentProvider, { requiresConsent: requiresConsent, cookieConsentConfig: resolvedConsent, children: [providedChildren, analyticsConfig && (_jsx(Suspense, { fallback: null, children: _jsx(CookieConsentAnalytics, { config: analyticsConfig }) })), analyticsConfig && (analyticsConfig.googleAnalyticsId || analyticsConfig.googleAdsId) && (_jsx(Suspense, { fallback: null, children: _jsx(AutoAnalyticsEvents, { config: autoAnalyticsEventsConfig ?? resolvedConsent?.autoAnalyticsEvents }) })), autoWireDialogs && (_jsxs(_Fragment, { children: [_jsx(Suspense, { fallback: null, children: _jsx(CookieConsentDialog, { ...(dialogProps ?? resolvedConsent?.dialogProps) }) }), _jsx(Suspense, { fallback: null, children: _jsx(PrivacyPolicyUpdateDialog, { ...(updateDialogProps ?? resolvedConsent?.updateDialogProps) }) })] }))] }));
34
+ providedChildren = (_jsxs(CookieConsentProvider, { requiresConsent: requiresConsent, cookieConsentConfig: resolvedConsent, children: [providedChildren, analyticsConfig && (_jsx(Suspense, { fallback: null, children: _jsx(CookieConsentAnalytics, { config: analyticsConfig }) })), analyticsConfig && (analyticsConfig.googleAnalyticsId || analyticsConfig.googleAdsId || (typeof analyticsConfig.googleTagId === 'string' && analyticsConfig.googleTagId)) && (_jsx(Suspense, { fallback: null, children: _jsx(AutoAnalyticsEvents, { config: autoAnalyticsEventsConfig ?? resolvedConsent?.autoAnalyticsEvents }) })), autoWireDialogs && (_jsxs(_Fragment, { children: [_jsx(Suspense, { fallback: null, children: _jsx(CookieConsentDialog, { ...(dialogProps ?? resolvedConsent?.dialogProps) }) }), _jsx(Suspense, { fallback: null, children: _jsx(PrivacyPolicyUpdateDialog, { ...(updateDialogProps ?? resolvedConsent?.updateDialogProps) }) })] }))] }));
35
35
  }
36
36
  return (_jsxs(_Fragment, { children: [_jsx(ClientThemeScript, {}), providedChildren] }));
37
37
  }
@@ -34,19 +34,30 @@ export default function CookieConsentAnalytics({ config }) {
34
34
  script.setAttribute('data-cf-beacon', config.cloudflareBeaconToken);
35
35
  document.head.appendChild(script);
36
36
  }, [granted, config.cloudflareBeaconToken]);
37
- const hasGoogle = Boolean(config.googleAnalyticsId || config.googleAdsId || config.googleAdSenseId);
37
+ const hasGoogle = Boolean(config.googleAnalyticsId ||
38
+ config.googleAdsId ||
39
+ config.googleAdSenseId ||
40
+ (typeof config.googleTagId === 'string' && config.googleTagId));
38
41
  return (_jsxs(_Fragment, { children: [hasGoogle && (_jsx("script", { id: "cookie-consent-google-consent-mode", dangerouslySetInnerHTML: { __html: googleConsentModeBootstrapScript(config) } })), hydrated && granted && config.clarityProjectId && (_jsx(Suspense, { fallback: null, children: _jsx(ClarityScript, { projectId: config.clarityProjectId }) }))] }));
39
42
  }
40
43
  export function googleConsentModeBootstrapScript(config) {
41
- const configCalls = [config.googleAnalyticsId, config.googleAdsId]
44
+ const configCalls = [
45
+ config.googleAnalyticsId,
46
+ config.googleAdsId,
47
+ typeof config.googleTagId === 'string' ? config.googleTagId : undefined,
48
+ ]
42
49
  .filter(Boolean)
50
+ .filter((id, index, arr) => arr.indexOf(id) === index)
43
51
  .map((id) => `gtag('config', '${id}');`)
44
52
  .join('\n');
45
- const gtagLoader = config.googleAnalyticsId || config.googleAdsId
53
+ const gtagTargetId = config.googleAnalyticsId ??
54
+ config.googleAdsId ??
55
+ (typeof config.googleTagId === 'string' ? config.googleTagId : undefined);
56
+ const gtagLoader = gtagTargetId
46
57
  ? `(function(){
47
58
  var s = document.createElement('script');
48
59
  s.async = true;
49
- s.src = 'https://www.googletagmanager.com/gtag/js?id=${config.googleAnalyticsId ?? config.googleAdsId}';
60
+ s.src = 'https://www.googletagmanager.com/gtag/js?id=${gtagTargetId}';
50
61
  document.head.appendChild(s);
51
62
  })();`
52
63
  : '';
@@ -17,6 +17,10 @@ function parseConsent(raw) {
17
17
  return null;
18
18
  }
19
19
  const defaultGdprCountriesSet = new Set(defaultGdprCountries);
20
+ const countryCookiePollMs = 200;
21
+ const countryCookieRevealMs = 2000;
22
+ const countryCookieMaxWaitMs = 15000;
23
+ const countryCookieRefreshParam = '__cfni_country';
20
24
  export default function CookieConsentProvider({ requiresConsent: requiresConsentProp = true, cookieConsentConfig, children, }) {
21
25
  const { consentCookieName, dateCookieName, maxAge, policyDate, privacyPolicyPath, showPrivacyPolicy, gdprCountries } = useMemo(() => {
22
26
  const cc = requireCookieConsentConfig(cookieConsentConfig ?? config.cookieConsent);
@@ -40,25 +44,48 @@ export default function CookieConsentProvider({ requiresConsent: requiresConsent
40
44
  const storedConsent = parseConsent(rawConsent);
41
45
  const isFirstVisit = rawConsent === null;
42
46
  setConsentState(storedConsent);
43
- if (requiresConsentProp) {
47
+ const gdprSet = gdprCountries ? new Set(gdprCountries) : defaultGdprCountriesSet;
48
+ const applyCountryCookie = () => {
44
49
  const countryCookie = getCookie(countryCookieKey);
45
- if (countryCookie) {
46
- const gdprSet = gdprCountries
47
- ? new Set(gdprCountries)
48
- : defaultGdprCountriesSet;
49
- setRequiresConsent(gdprSet.has(countryCookie));
50
- }
50
+ if (!countryCookie)
51
+ return false;
52
+ setRequiresConsent(gdprSet.has(countryCookie));
53
+ return true;
54
+ };
55
+ let pollId;
56
+ let revealId;
57
+ let stopId;
58
+ const stopPolling = () => {
59
+ clearInterval(pollId);
60
+ clearTimeout(revealId);
61
+ clearTimeout(stopId);
62
+ };
63
+ if (!requiresConsentProp || applyCountryCookie()) {
64
+ setIsMounted(true);
51
65
  }
52
- setIsMounted(true);
53
- if (isFirstVisit || !policyDate)
54
- return;
55
- const storedDateRaw = getCookie(dateCookieName);
56
- if (!storedDateRaw) {
57
- setCookie({ name: dateCookieName, value: policyDate.toISOString(), maxAge });
58
- return;
66
+ else {
67
+ fetch(`${window.location.pathname}?${countryCookieRefreshParam}=${Date.now()}`, { method: 'HEAD', cache: 'no-store', credentials: 'same-origin' })
68
+ .catch(() => undefined);
69
+ pollId = setInterval(() => {
70
+ if (!applyCountryCookie())
71
+ return;
72
+ stopPolling();
73
+ setIsMounted(true);
74
+ }, countryCookiePollMs);
75
+ revealId = setTimeout(() => setIsMounted(true), countryCookieRevealMs);
76
+ stopId = setTimeout(stopPolling, countryCookieMaxWaitMs);
77
+ }
78
+ if (!isFirstVisit && policyDate) {
79
+ const storedDateRaw = getCookie(dateCookieName);
80
+ if (!storedDateRaw) {
81
+ setCookie({ name: dateCookieName, value: policyDate.toISOString(), maxAge });
82
+ }
83
+ else {
84
+ const storedDate = new Date(storedDateRaw);
85
+ setPrivacyPolicyUpdated(!Number.isNaN(storedDate.getTime()) && storedDate < policyDate);
86
+ }
59
87
  }
60
- const storedDate = new Date(storedDateRaw);
61
- setPrivacyPolicyUpdated(!Number.isNaN(storedDate.getTime()) && storedDate < policyDate);
88
+ return stopPolling;
62
89
  }, [consentCookieName, dateCookieName, maxAge, policyDate, requiresConsentProp, gdprCountries]);
63
90
  const setConsent = useCallback((value) => {
64
91
  if (value === null) {
@@ -1,6 +1,9 @@
1
1
  import React from 'react';
2
2
  import { defaultReloadHtml } from 'cloudflare-next-intl-errors/staleDeployScripts';
3
3
  export { defaultReloadHtml };
4
- export default function HelperScript({ locale }: {
4
+ export interface HelperScriptProps {
5
5
  locale?: string;
6
- }): React.JSX.Element;
6
+ googleTag?: boolean;
7
+ googleTagId?: string | false;
8
+ }
9
+ export default function HelperScript({ locale, googleTag, googleTagId, }?: HelperScriptProps): React.JSX.Element;
@@ -1,12 +1,28 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import CoreHelperScript from 'cloudflare-next-intl-intl/internal/helperScript';
3
3
  import ThemeScript from 'cloudflare-next-intl-theme/ThemeScript';
4
4
  import StaleDeployScripts, { defaultReloadHtml } from 'cloudflare-next-intl-errors/staleDeployScripts';
5
5
  import config from '../../config/intl_config.js';
6
6
  export { defaultReloadHtml };
7
- export default function HelperScript({ locale }) {
7
+ export default function HelperScript({ locale, googleTag, googleTagId, } = {}) {
8
8
  const analytics = config.cookieConsent?.analytics;
9
- const hasGoogle = !!(analytics?.googleAnalyticsId || analytics?.googleAdsId || analytics?.googleAdSenseId);
9
+ const cookieConsent = config.cookieConsent;
10
+ const isGoogleTagExplicitlyOff = googleTag === false ||
11
+ googleTagId === false ||
12
+ analytics?.googleTag === false ||
13
+ analytics?.googleTagId === false ||
14
+ cookieConsent?.googleTag === false ||
15
+ cookieConsent?.googleTagId === false;
16
+ const rawTagId = (typeof googleTagId === 'string' ? googleTagId : undefined) ||
17
+ (typeof analytics?.googleTagId === 'string' ? analytics.googleTagId : undefined) ||
18
+ (typeof cookieConsent?.googleTagId === 'string' ? cookieConsent.googleTagId : undefined) ||
19
+ analytics?.googleAdsId;
20
+ const resolvedGoogleTagId = !isGoogleTagExplicitlyOff ? rawTagId : undefined;
21
+ const shouldRenderGoogleTag = Boolean(resolvedGoogleTagId && !isGoogleTagExplicitlyOff);
22
+ const hasGoogle = !!(analytics?.googleAnalyticsId ||
23
+ analytics?.googleAdsId ||
24
+ analytics?.googleAdSenseId ||
25
+ shouldRenderGoogleTag);
10
26
  const hasClarity = !!analytics?.clarityProjectId;
11
27
  const hasFirebase = !!config.firebaseAuth;
12
28
  const hasTurnstile = !!config.firebaseAuth?.appCheck?.turnstileSiteKey;
@@ -22,5 +38,7 @@ export default function HelperScript({ locale }) {
22
38
  const preconnectOrigins = [];
23
39
  if (hasTurnstile)
24
40
  preconnectOrigins.push('https://challenges.cloudflare.com');
25
- return (_jsxs(CoreHelperScript, { locale: locale, prefetchOrigins: prefetchOrigins.length > 0 ? prefetchOrigins : undefined, preconnectOrigins: preconnectOrigins.length > 0 ? preconnectOrigins : undefined, children: [_jsx(ThemeScript, {}), _jsx(StaleDeployScripts, { reloadHtml: config.errorHandling?.staleDeployReloadHtml })] }));
41
+ return (_jsxs(CoreHelperScript, { locale: locale, prefetchOrigins: prefetchOrigins.length > 0 ? prefetchOrigins : undefined, preconnectOrigins: preconnectOrigins.length > 0 ? preconnectOrigins : undefined, children: [_jsx(ThemeScript, {}), _jsx(StaleDeployScripts, { reloadHtml: config.errorHandling?.staleDeployReloadHtml }), shouldRenderGoogleTag && (_jsxs(_Fragment, { children: [_jsx("script", { async: true, src: `https://www.googletagmanager.com/gtag/js?id=${resolvedGoogleTagId}` }), _jsx("script", { id: "google-tag-init", dangerouslySetInnerHTML: {
42
+ __html: `window.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\ngtag('js', new Date());\ngtag('config', '${resolvedGoogleTagId}');`,
43
+ } })] }))] }));
26
44
  }
@@ -20,6 +20,8 @@ export interface CookieConsentRoutingConfig {
20
20
  privacyPolicyDateCookieName?: string;
21
21
  cookieMaxAge?: number;
22
22
  autoWireAnalytics?: boolean;
23
+ googleTag?: boolean;
24
+ googleTagId?: string | false;
23
25
  analytics?: CookieConsentAnalyticsConfig;
24
26
  getAnalytics?: () => CookieConsentAnalyticsConfig | Promise<CookieConsentAnalyticsConfig>;
25
27
  autoAnalyticsEvents?: AutoAnalyticsEventsConfig;
@@ -55,6 +57,8 @@ export interface CookieConsentAnalyticsConfig {
55
57
  cloudflareBeaconToken?: string;
56
58
  googleAnalyticsId?: string;
57
59
  googleAdsId?: string;
60
+ googleTagId?: string | false;
61
+ googleTag?: boolean;
58
62
  googleAdSenseId?: string;
59
63
  clarityProjectId?: string;
60
64
  }
package/llms.txt CHANGED
@@ -16,7 +16,7 @@ other subpath can be used.
16
16
  - `./setIntlConfig` — identity function for typed `RoutingConfig` authoring; use in your `@intl-config` file.
17
17
  - `./serverProvider` — `IntlProvider` server component (also under `./server`).
18
18
  - `./Link` — server-side locale-aware `<Link>` (also under `./server`).
19
- - `./IntlHelperScript` — inline script for flash-free locale/theme hydration.
19
+ - `./IntlHelperScript` — inline script for flash-free locale/theme hydration; also renders the Google tag (gtag.js) for `cookieConsent.googleTagId` (defaults to `analytics.googleAdsId`; `googleTag: false` or `googleTagId: false` turns it off).
20
20
  - `./LocaleLink` — client-side locale-aware link (counterpart to `./Link`).
21
21
  - `./usePathname` — client hook: pathname with locale prefix stripped.
22
22
  - `./metadata` — Next.js `generateMetadata` helper with locale alternates.
@@ -63,8 +63,8 @@ state then isn't shared. Peer resolution guarantees one shared copy instead.
63
63
  - `./useCookieConsent` — context hook; throws `"useCookieConsent must be used within a CookieConsentProvider"` if called outside one.
64
64
  - `./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.
65
65
  - `./PrivacyPolicyUpdateDialog` — "privacy policy updated" banner; auto-enabled only when `cookieConsent.privacyPolicyDate` is set. Same default-link behavior as `CookieConsentDialog` (label default `"Learn more"`).
66
- - `./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`.
67
- - 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).
66
+ - `./cookieConsentAnalytics` — `CookieConsentAnalytics`: gates Cloudflare Web Analytics / Google Ads / Google Analytics / Google tag (`googleTagId`) / AdSense / Microsoft Clarity behind consent; loads gtag when any of `googleAnalyticsId`, `googleAdsId` or `googleTagId` is set; 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`.
67
+ - 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). On edge-cached pages `CookieConsentProvider` re-evaluates from the `__cf_country__` cookie set by `intlMiddleware`; on a first-visit cache hit it sends one cache-busting `HEAD` request to fetch the cookie and keeps the dialog hidden for up to 2 s while waiting.
68
68
 
69
69
  ## `db*` subpaths (require `db` set on your `RoutingConfig`)
70
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.13.1",
3
+ "version": "0.13.3",
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",
@@ -364,7 +364,7 @@
364
364
  "homepage": "https://github.com/demian-ilnytskyi/cloudflare-next-intl#readme",
365
365
  "dependencies": {
366
366
  "@microsoft/clarity": "^1.0.2",
367
- "cloudflare-next-intl-auth": "^0.2.1",
367
+ "cloudflare-next-intl-auth": "^0.3.0",
368
368
  "cloudflare-next-intl-db": "^0.3.0",
369
369
  "cloudflare-next-intl-db-codegen": "^0.1.4",
370
370
  "cloudflare-next-intl-errors": "^0.1.0",