@owlmeans/consent 0.1.18-rc.9 → 0.1.18
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 +229 -0
- package/build/consts.d.ts +78 -3
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +104 -0
- package/build/consts.js.map +1 -1
- package/build/consts.local.d.ts +12 -0
- package/build/consts.local.d.ts.map +1 -0
- package/build/consts.local.js +12 -0
- package/build/consts.local.js.map +1 -0
- package/build/geo/types.d.ts +47 -0
- package/build/geo/types.d.ts.map +1 -0
- package/build/geo/types.js +2 -0
- package/build/geo/types.js.map +1 -0
- package/build/geo.d.ts +4 -0
- package/build/geo.d.ts.map +1 -0
- package/build/geo.js +96 -0
- package/build/geo.js.map +1 -0
- package/build/gtobserver/types.d.ts +75 -0
- package/build/gtobserver/types.d.ts.map +1 -0
- package/build/gtobserver/types.js +2 -0
- package/build/gtobserver/types.js.map +1 -0
- package/build/gtobserver.d.ts +7 -0
- package/build/gtobserver.d.ts.map +1 -0
- package/build/gtobserver.js +142 -0
- package/build/gtobserver.js.map +1 -0
- package/build/i18n/be.json +8 -1
- package/build/i18n/de.json +8 -1
- package/build/i18n/en.json +8 -1
- package/build/i18n/es.json +8 -1
- package/build/i18n/fr.json +8 -1
- package/build/i18n/pl.json +8 -1
- package/build/i18n/ru.json +8 -1
- package/build/i18n/types.d.ts +11 -0
- package/build/i18n/types.d.ts.map +1 -0
- package/build/i18n/types.js +2 -0
- package/build/i18n/types.js.map +1 -0
- package/build/i18n/uk.json +8 -1
- package/build/i18n.d.ts +4 -14
- package/build/i18n.d.ts.map +1 -1
- package/build/i18n.js +12 -27
- package/build/i18n.js.map +1 -1
- package/build/index.d.ts +13 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -3
- package/build/index.js.map +1 -1
- package/build/linker/types.d.ts +59 -0
- package/build/linker/types.d.ts.map +1 -0
- package/build/linker/types.js +2 -0
- package/build/linker/types.js.map +1 -0
- package/build/linker.d.ts +5 -74
- package/build/linker.d.ts.map +1 -1
- package/build/linker.js +290 -324
- package/build/linker.js.map +1 -1
- package/build/plugins/types.d.ts +42 -0
- package/build/plugins/types.d.ts.map +1 -0
- package/build/plugins/types.js +2 -0
- package/build/plugins/types.js.map +1 -0
- package/build/plugins.d.ts +4 -68
- package/build/plugins.d.ts.map +1 -1
- package/build/plugins.js +73 -61
- package/build/plugins.js.map +1 -1
- package/build/storage/types.d.ts +31 -0
- package/build/storage/types.d.ts.map +1 -0
- package/build/storage/types.js +2 -0
- package/build/storage/types.js.map +1 -0
- package/build/storage.d.ts +6 -22
- package/build/storage.d.ts.map +1 -1
- package/build/storage.js +72 -81
- package/build/storage.js.map +1 -1
- package/build/store.d.ts +2 -2
- package/build/store.d.ts.map +1 -1
- package/build/store.js +115 -31
- package/build/store.js.map +1 -1
- package/build/types.d.ts +141 -5
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +9 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/package.json +3 -3
- package/src/consts.local.ts +16 -0
- package/src/consts.ts +120 -3
- package/src/geo/types.ts +49 -0
- package/src/geo.ts +122 -0
- package/src/gtobserver/types.ts +73 -0
- package/src/gtobserver.ts +182 -0
- package/src/i18n/be.json +8 -1
- package/src/i18n/de.json +8 -1
- package/src/i18n/en.json +8 -1
- package/src/i18n/es.json +8 -1
- package/src/i18n/fr.json +8 -1
- package/src/i18n/pl.json +8 -1
- package/src/i18n/ru.json +8 -1
- package/src/i18n/types.ts +11 -0
- package/src/i18n/uk.json +8 -1
- package/src/i18n.ts +20 -32
- package/src/index.ts +13 -12
- package/src/linker/types.ts +59 -0
- package/src/linker.ts +301 -349
- package/src/plugins/types.ts +42 -0
- package/src/plugins.ts +82 -105
- package/src/storage/types.ts +31 -0
- package/src/storage.ts +75 -77
- package/src/store.ts +125 -31
- package/src/types.local.ts +9 -0
- package/src/types.ts +148 -5
- package/tests/consent.spec.ts +26 -28
- package/tests/geo.spec.ts +474 -0
- package/tests/linker.spec.ts +68 -74
- package/tests/tracking.spec.ts +14 -14
- package/build/gtm.d.ts +0 -72
- package/build/gtm.d.ts.map +0 -1
- package/build/gtm.js +0 -182
- package/build/gtm.js.map +0 -1
- package/src/gtm.ts +0 -220
package/src/consts.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import type { ConsentCategory } from './types.js'
|
|
1
|
+
import type { ConsentCategory, ConsentLocale, ConsentState } from './types.js'
|
|
2
|
+
|
|
3
|
+
import en from './i18n/en.json' with { type: 'json' }
|
|
4
|
+
import pl from './i18n/pl.json' with { type: 'json' }
|
|
5
|
+
import ru from './i18n/ru.json' with { type: 'json' }
|
|
6
|
+
import be from './i18n/be.json' with { type: 'json' }
|
|
7
|
+
import uk from './i18n/uk.json' with { type: 'json' }
|
|
8
|
+
import es from './i18n/es.json' with { type: 'json' }
|
|
9
|
+
import de from './i18n/de.json' with { type: 'json' }
|
|
10
|
+
import fr from './i18n/fr.json' with { type: 'json' }
|
|
2
11
|
|
|
3
12
|
/**
|
|
4
13
|
* Where a visitor's choice is stored.
|
|
@@ -46,9 +55,19 @@ export const CONSENT_MARKETING = 'marketing'
|
|
|
46
55
|
*/
|
|
47
56
|
export const CONSENT_LANGUAGE_KEY = 'owlmeans-lng'
|
|
48
57
|
|
|
49
|
-
export const CONSENT_LOCALES = ['en', 'pl', 'ru', 'be', 'uk', 'es', 'de', 'fr'] as const
|
|
58
|
+
export const CONSENT_LOCALES = ['en', 'pl', 'ru', 'be', 'uk', 'es', 'de', 'fr'] as const satisfies readonly ConsentLocale[]
|
|
50
59
|
|
|
51
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The dialog's copy, in the box.
|
|
62
|
+
*
|
|
63
|
+
* Carried here rather than registered into an i18n framework because one of the three consumers is
|
|
64
|
+
* an Astro site with React islands and no OwlMeans i18n at all — and a consent dialog that renders
|
|
65
|
+
* raw keys is worse than no dialog. An application that HAS translations passes `translate` and
|
|
66
|
+
* overrides every one of these.
|
|
67
|
+
*/
|
|
68
|
+
export const DEFAULT_CONSENT_MESSAGES: Record<ConsentLocale, Record<string, string>> = {
|
|
69
|
+
en, pl, ru, be, uk, es, de, fr,
|
|
70
|
+
}
|
|
52
71
|
|
|
53
72
|
/**
|
|
54
73
|
* The categories every OwlMeans surface starts with.
|
|
@@ -94,3 +113,101 @@ export const CONSENT_SIGNAL_DEFAULTS: Record<string, 'granted' | 'denied'> = {
|
|
|
94
113
|
personalization_storage: 'denied',
|
|
95
114
|
security_storage: 'granted',
|
|
96
115
|
}
|
|
116
|
+
|
|
117
|
+
/** The URL parameter a decorated link carries the decision in, absent an override. */
|
|
118
|
+
export const CONSENT_LINK_PARAM = 'owlcc'
|
|
119
|
+
|
|
120
|
+
/** How old a decorated link's timestamp may be and still be trusted, in seconds, absent an override. */
|
|
121
|
+
export const CONSENT_LINK_MAX_AGE = 300
|
|
122
|
+
|
|
123
|
+
/** Clock skew allowed the OTHER way — a timestamp up to this far in the future is still trusted. */
|
|
124
|
+
export const CONSENT_LINK_SKEW = 60
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Where prior consent for non-essential cookies is the law: the EU and EEA (ePrivacy Art. 5(3) with
|
|
128
|
+
* the GDPR), including the parts of member states that carry an ISO code of their own — Åland and
|
|
129
|
+
* the French outermost regions (French Guiana, Guadeloupe, Martinique, Réunion, Mayotte,
|
|
130
|
+
* Saint-Martin). The Canary Islands, Azores, Madeira, Ceuta and Melilla report their state's code.
|
|
131
|
+
* Greece is `GR`, as Cloudflare and every ISO feed spell it.
|
|
132
|
+
*/
|
|
133
|
+
export const CONSENT_COUNTRIES_GDPR: readonly string[] = Object.freeze([
|
|
134
|
+
'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GR', 'HR', 'HU',
|
|
135
|
+
'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK',
|
|
136
|
+
'AX', 'GF', 'GP', 'MQ', 'RE', 'YT', 'MF',
|
|
137
|
+
'IS', 'LI', 'NO',
|
|
138
|
+
])
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Jurisdictions outside the EEA whose rule for cookies is the same prior opt-in, by statute or by
|
|
142
|
+
* the regulator's guidance (2026):
|
|
143
|
+
*
|
|
144
|
+
* - the United Kingdom (PECR — the 2025 Data (Use and Access) Act exempts some first-party
|
|
145
|
+
* analytics, advertising still needs consent), the Crown Dependencies and Gibraltar;
|
|
146
|
+
* - Switzerland (FDPIC guidelines, 2025: explicit consent for tracking and profiling);
|
|
147
|
+
* - Andorra, Monaco, San Marino and the Vatican; Türkiye (KVKK cookie guide); Serbia (ZZPL and the
|
|
148
|
+
* electronic-communications law); Albania, Bosnia and Herzegovina, Montenegro, North Macedonia,
|
|
149
|
+
* Kosovo, Moldova, Georgia and Ukraine (GDPR-modelled laws);
|
|
150
|
+
* - territories under a member state's regulator but outside the EU: the Faroe Islands and
|
|
151
|
+
* Greenland (Denmark); Saint-Barthélemy, Saint-Pierre-et-Miquelon, New Caledonia, French
|
|
152
|
+
* Polynesia, Wallis and Futuna (France); the Caribbean Netherlands.
|
|
153
|
+
*/
|
|
154
|
+
export const CONSENT_COUNTRIES_ALIGNED: readonly string[] = Object.freeze([
|
|
155
|
+
'GB', 'GG', 'JE', 'IM', 'GI',
|
|
156
|
+
'CH',
|
|
157
|
+
'AD', 'MC', 'SM', 'VA', 'TR', 'RS', 'AL', 'BA', 'ME', 'MK', 'XK', 'MD', 'GE', 'UA',
|
|
158
|
+
'FO', 'GL', 'BL', 'PM', 'NC', 'PF', 'WF', 'BQ',
|
|
159
|
+
])
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Prior opt-in regimes elsewhere (2026): Brazil (LGPD, ANPD cookie guide), Canada (Québec's Law 25
|
|
163
|
+
* — the country is the finest grain a CDN reports, so all of Canada), China (PIPL), South Korea
|
|
164
|
+
* (PIPA), Nigeria (NDPA), Saudi Arabia (PDPL), Thailand (PDPA), Vietnam (PDPL 2026).
|
|
165
|
+
*
|
|
166
|
+
* Deliberately absent: India (the DPDP Act's consent rules apply from mid-2027 — add `IN` then),
|
|
167
|
+
* and the notice-or-opt-out regimes — the United States, Japan, Australia, Singapore. South
|
|
168
|
+
* Africa's POPIA is read both ways by the guides and is left out until its regulator says more.
|
|
169
|
+
*/
|
|
170
|
+
export const CONSENT_COUNTRIES_OPT_IN: readonly string[] = Object.freeze([
|
|
171
|
+
'BR', 'CA', 'CN', 'KR', 'NG', 'SA', 'TH', 'VN',
|
|
172
|
+
])
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Every country where a located visitor is ASKED (`ConsentGeoOptions.countries` replaces it). A
|
|
176
|
+
* visitor located anywhere else gets an automatic decision; one who cannot be located is asked.
|
|
177
|
+
*/
|
|
178
|
+
export const CONSENT_REQUIRED_COUNTRIES: readonly string[] = Object.freeze([
|
|
179
|
+
...CONSENT_COUNTRIES_GDPR, ...CONSENT_COUNTRIES_ALIGNED, ...CONSENT_COUNTRIES_OPT_IN,
|
|
180
|
+
])
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Codes that name no country: Cloudflare's `XX` (no data) and `T1` (Tor), the retired MaxMind
|
|
184
|
+
* pseudo-codes (`A1` anonymous proxy, `A2` satellite, `O1` other) and the continent buckets some
|
|
185
|
+
* feeds fall back to (`EU`, `AP`), plus `ZZ` (unknown). A visitor reported with one is asked.
|
|
186
|
+
*/
|
|
187
|
+
export const CONSENT_GEO_UNKNOWN: readonly string[] = Object.freeze([
|
|
188
|
+
'XX', 'T1', 'A1', 'A2', 'O1', 'EU', 'AP', 'ZZ',
|
|
189
|
+
])
|
|
190
|
+
|
|
191
|
+
/** Cloudflare's trace endpoint — served by the edge on every proxied host, `loc=` among its lines. */
|
|
192
|
+
export const CONSENT_TRACE_PATH = '/cdn-cgi/trace'
|
|
193
|
+
|
|
194
|
+
/** How long locating may take before the visitor is asked anyway, in ms. */
|
|
195
|
+
export const CONSENT_GEO_TIMEOUT = 2500
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* How long an automatic decision is trusted, in seconds. Older, it counts as undecided — in the
|
|
199
|
+
* head scripts as much as in the store — until the visitor is located again, so a grant derived in
|
|
200
|
+
* one country never reaches a page opened an hour later in another.
|
|
201
|
+
*/
|
|
202
|
+
export const CONSENT_AUTO_MAX_AGE = 3600
|
|
203
|
+
|
|
204
|
+
/** The state of a store nobody has initialised — and the only server-side snapshot. */
|
|
205
|
+
export const CONSENT_IDLE_STATE: ConsentState = Object.freeze({
|
|
206
|
+
record: null, open: false, reason: null, locating: null,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The attribute on `<html>` that mirrors the consent phase: `locating`, `open`, `decided` or `idle`.
|
|
211
|
+
* Absent until the store first runs. A test waits on it, and a stylesheet may key on it.
|
|
212
|
+
*/
|
|
213
|
+
export const CONSENT_STATE_ATTRIBUTE = 'data-consent'
|
package/src/geo/types.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ConsentGeoPlugin, ConsentOptions, ConsentRecord } from '../types.js'
|
|
2
|
+
|
|
3
|
+
/** What locating the visitor decided: ask them, or decide for them automatically. */
|
|
4
|
+
export type ConsentGeoVerdict = 'ask' | 'auto'
|
|
5
|
+
|
|
6
|
+
/** Whether a record is an automatic decision, and whether it is still trusted. */
|
|
7
|
+
export type ConsentAutoState = 'fresh' | 'stale'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Asking only where the law requires it: locating the visitor, the decision that follows, and the
|
|
11
|
+
* automatic record written where nobody needs to be asked.
|
|
12
|
+
*/
|
|
13
|
+
export interface ConsentGeoHelper {
|
|
14
|
+
/** Whether `opts.geo` turns the gate on AND something can answer — the Cloudflare locator or a plugin. */
|
|
15
|
+
enabled: (opts?: ConsentOptions) => boolean
|
|
16
|
+
/** Every `key=value` line of a Cloudflare trace body. Never throws; anything else parses to `{}`. */
|
|
17
|
+
parseTrace: (text: string) => Record<string, string>
|
|
18
|
+
/**
|
|
19
|
+
* The built-in locator: a same-origin GET of the trace endpoint, cookies left out, never cached.
|
|
20
|
+
* Rejects when `opts.geo.cloudflare` is off, the request fails or answers non-2xx, or the body is
|
|
21
|
+
* not a trace (an SPA serving its `index.html` for every path answers 200 here).
|
|
22
|
+
*/
|
|
23
|
+
cloudflareLocator: () => ConsentGeoPlugin
|
|
24
|
+
/**
|
|
25
|
+
* Whether a visitor located in `country` must be asked: yes inside `opts.geo.countries`
|
|
26
|
+
* (`CONSENT_REQUIRED_COUNTRIES`), and yes for anything that names no country.
|
|
27
|
+
*/
|
|
28
|
+
requiresConsent: (country: string | null, opts?: ConsentOptions) => boolean
|
|
29
|
+
/**
|
|
30
|
+
* Locate and decide, bounded by `opts.geo.timeout`. Never rejects: a visitor nobody could locate
|
|
31
|
+
* resolves `'ask'`.
|
|
32
|
+
*/
|
|
33
|
+
decide: (opts: ConsentOptions) => Promise<ConsentGeoVerdict>
|
|
34
|
+
/**
|
|
35
|
+
* Whether the browser sends Global Privacy Control — a legally binding opt-out in several US
|
|
36
|
+
* states, so an automatic decision honours it.
|
|
37
|
+
*/
|
|
38
|
+
privacySignal: () => boolean
|
|
39
|
+
/**
|
|
40
|
+
* The automatic decision for a visitor nobody needs to ask: every category on — or, under Global
|
|
41
|
+
* Privacy Control, only the required ones — stamped `auto` with the current time.
|
|
42
|
+
*/
|
|
43
|
+
automaticRecord: (opts?: ConsentOptions) => ConsentRecord
|
|
44
|
+
/**
|
|
45
|
+
* `fresh` for an automatic decision younger than `CONSENT_AUTO_MAX_AGE`, `stale` for an older one
|
|
46
|
+
* (or one stamped in the future beyond the skew), `null` for an explicit record or none.
|
|
47
|
+
*/
|
|
48
|
+
autoState: (record: ConsentRecord | null) => ConsentAutoState | null
|
|
49
|
+
}
|
package/src/geo.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONSENT_AUTO_MAX_AGE, CONSENT_GEO_TIMEOUT, CONSENT_GEO_UNKNOWN, CONSENT_REQUIRED_COUNTRIES,
|
|
3
|
+
CONSENT_SCHEMA_VERSION, CONSENT_TRACE_PATH, DEFAULT_CONSENT_CATEGORIES,
|
|
4
|
+
} from './consts.js'
|
|
5
|
+
import {
|
|
6
|
+
AUTO_SKEW, CLOUDFLARE_LOCATOR_ALIAS, CLOUDFLARE_LOCATOR_PRIORITY, COUNTRY_CODE,
|
|
7
|
+
} from './consts.local.js'
|
|
8
|
+
import { consentPluginHelper } from './plugins.js'
|
|
9
|
+
import type { ConsentGeoPlugin, ConsentOptions, ConsentRecord } from './types.js'
|
|
10
|
+
import type { ConsentNavigator } from './types.local.js'
|
|
11
|
+
import type { ConsentAutoState, ConsentGeoHelper, ConsentGeoVerdict } from './geo/types.js'
|
|
12
|
+
|
|
13
|
+
export const createConsentGeoHelper = (): ConsentGeoHelper => {
|
|
14
|
+
const now = (): number => Math.floor(Date.now() / 1000)
|
|
15
|
+
|
|
16
|
+
const enabled = (opts?: ConsentOptions): boolean => {
|
|
17
|
+
const geo = opts?.geo
|
|
18
|
+
if (geo == null) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (geo.cloudflare != null && geo.cloudflare !== false)
|
|
23
|
+
|| consentPluginHelper.consentPlugins().some(plugin => plugin.locate != null
|
|
24
|
+
&& plugin.alias !== CLOUDFLARE_LOCATOR_ALIAS)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const parseTrace = (text: string): Record<string, string> => {
|
|
28
|
+
const trace: Record<string, string> = {}
|
|
29
|
+
for (const line of text.split(/\r?\n/)) {
|
|
30
|
+
const at = line.indexOf('=')
|
|
31
|
+
if (at > 0 && /^[a-z_]+$/.test(line.slice(0, at))) {
|
|
32
|
+
trace[line.slice(0, at)] = line.slice(at + 1).trim()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return trace
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const cloudflareLocator = (): ConsentGeoPlugin => ({
|
|
40
|
+
alias: CLOUDFLARE_LOCATOR_ALIAS,
|
|
41
|
+
priority: CLOUDFLARE_LOCATOR_PRIORITY,
|
|
42
|
+
locate: async opts => {
|
|
43
|
+
const setting = opts.geo?.cloudflare
|
|
44
|
+
if (setting == null || setting === false) {
|
|
45
|
+
throw new Error('consent:geo:cloudflare:off')
|
|
46
|
+
}
|
|
47
|
+
if (typeof fetch !== 'function') {
|
|
48
|
+
throw new Error('consent:geo:cloudflare:no-fetch')
|
|
49
|
+
}
|
|
50
|
+
const path = typeof setting === 'object' ? setting.path ?? CONSENT_TRACE_PATH : CONSENT_TRACE_PATH
|
|
51
|
+
const response = await fetch(path, { credentials: 'omit', cache: 'no-store' })
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new Error(`consent:geo:cloudflare:status:${response.status}`)
|
|
54
|
+
}
|
|
55
|
+
const trace = parseTrace(await response.text())
|
|
56
|
+
// Every trace names the edge that answered; an HTML fallback page names nothing at all.
|
|
57
|
+
if (trace.loc == null || (trace.colo == null && trace.fl == null)) {
|
|
58
|
+
throw new Error('consent:geo:cloudflare:not-a-trace')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { country: trace.loc }
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const requiresConsent = (country: string | null, opts?: ConsentOptions): boolean => {
|
|
66
|
+
const code = country?.trim().toUpperCase() ?? ''
|
|
67
|
+
if (!COUNTRY_CODE.test(code) || CONSENT_GEO_UNKNOWN.includes(code)) {
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (opts?.geo?.countries ?? CONSENT_REQUIRED_COUNTRIES)
|
|
72
|
+
.some(candidate => candidate.toUpperCase() === code)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const decide = async (opts: ConsentOptions): Promise<ConsentGeoVerdict> => {
|
|
76
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
77
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
78
|
+
timer = setTimeout(
|
|
79
|
+
() => reject(new Error('consent:geo:timeout')), opts.geo?.timeout ?? CONSENT_GEO_TIMEOUT
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
try {
|
|
83
|
+
const location = await Promise.race([consentPluginHelper.locateConsent(opts), timeout])
|
|
84
|
+
|
|
85
|
+
return requiresConsent(location.country, opts) ? 'ask' : 'auto'
|
|
86
|
+
} catch {
|
|
87
|
+
return 'ask'
|
|
88
|
+
} finally {
|
|
89
|
+
clearTimeout(timer)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const privacySignal = (): boolean =>
|
|
94
|
+
typeof navigator !== 'undefined'
|
|
95
|
+
&& (navigator as unknown as ConsentNavigator).globalPrivacyControl === true
|
|
96
|
+
|
|
97
|
+
const automaticRecord = (opts?: ConsentOptions): ConsentRecord => {
|
|
98
|
+
const optOut = privacySignal()
|
|
99
|
+
const record: ConsentRecord = { v: CONSENT_SCHEMA_VERSION, auto: now() }
|
|
100
|
+
for (const category of opts?.categories ?? DEFAULT_CONSENT_CATEGORIES) {
|
|
101
|
+
record[category.key] = category.required === true || !optOut
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return record
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const autoState = (record: ConsentRecord | null): ConsentAutoState | null => {
|
|
108
|
+
if (record == null || typeof record.auto !== 'number') {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
const age = now() - record.auto
|
|
112
|
+
|
|
113
|
+
return age <= CONSENT_AUTO_MAX_AGE && age >= -AUTO_SKEW ? 'fresh' : 'stale'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
enabled, parseTrace, cloudflareLocator, requiresConsent, decide, privacySignal, automaticRecord,
|
|
118
|
+
autoState,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const consentGeoHelper = createConsentGeoHelper()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ConsentCategory, ConsentOptions, ConsentRecord } from '../types.js'
|
|
2
|
+
|
|
3
|
+
/** Google Consent Mode: the queue, the signals a decision implies, and the inline scripts. */
|
|
4
|
+
export interface ConsentModeHelper {
|
|
5
|
+
/**
|
|
6
|
+
* Push onto the tag-manager queue in the shape Google's own snippet uses.
|
|
7
|
+
*
|
|
8
|
+
* `arguments`, not an array literal. Both work with today's Consent API, but `arguments` is what
|
|
9
|
+
* `gtag.js` itself emits, and a page that carries both this and Google's snippet should not have
|
|
10
|
+
* two shapes in one queue.
|
|
11
|
+
*/
|
|
12
|
+
gtagConsent: (..._args: unknown[]) => void
|
|
13
|
+
/** Every signal the configured categories drive, at its pre-consent value. */
|
|
14
|
+
consentDefaults: (categories?: ConsentCategory[]) => Record<string, 'granted' | 'denied'>
|
|
15
|
+
/** Every signal, at the value this record implies. */
|
|
16
|
+
consentUpdate: (record: ConsentRecord, categories?: ConsentCategory[]) => Record<string, 'granted' | 'denied'>
|
|
17
|
+
/**
|
|
18
|
+
* Declare what is denied, before any tag can act.
|
|
19
|
+
*
|
|
20
|
+
* Idempotent through a window flag, because the page may carry this call twice — once inline in
|
|
21
|
+
* the document head, once from whatever bundle mounts the dialog — and a second `default` after a
|
|
22
|
+
* tag has loaded is worse than none: it can widen what was already narrowed.
|
|
23
|
+
*/
|
|
24
|
+
pushConsentDefaults: (opts?: ConsentOptions) => void
|
|
25
|
+
/**
|
|
26
|
+
* Apply a decision: globals first, then the signal update, then any per-category events.
|
|
27
|
+
*
|
|
28
|
+
* The order is load-bearing. A tag that fires on the update event reads the globals in the same
|
|
29
|
+
* turn, so writing them afterwards would let the first firing see the previous answer.
|
|
30
|
+
*/
|
|
31
|
+
applyConsent: (record: ConsentRecord, opts?: ConsentOptions) => void
|
|
32
|
+
/**
|
|
33
|
+
* Whether a stored decision grants tracking at all — a signal-bearing category, not merely the
|
|
34
|
+
* required essential one.
|
|
35
|
+
*
|
|
36
|
+
* "Signal-bearing" is what a required category can never be: it exists to gate whether a LOADER
|
|
37
|
+
* runs, and a required category is disclosure, not a question — everyone gets it. Only a category
|
|
38
|
+
* that both is optional and drives at least one Consent Mode signal counts, which is exactly what
|
|
39
|
+
* `googleTagHeadScript`'s `'basic'` mode gates a Google tag behind.
|
|
40
|
+
*/
|
|
41
|
+
trackingGranted: (record: ConsentRecord | null, categories?: ConsentCategory[]) => boolean
|
|
42
|
+
/**
|
|
43
|
+
* The inline script a document stamps ABOVE its tag-manager snippet.
|
|
44
|
+
*
|
|
45
|
+
* It exists because the defaults have to be on the queue before `gtm.js` loads, and a React bundle
|
|
46
|
+
* cannot be: by the time an island mounts, the container has been running for hundreds of
|
|
47
|
+
* milliseconds and has already decided what it may do. Emitting it from the HTML is the only
|
|
48
|
+
* ordering that holds — and it reads the stored record too, so a returning visitor's tags are not
|
|
49
|
+
* denied for the first paint of every page.
|
|
50
|
+
*/
|
|
51
|
+
consentBootstrapScript: (opts?: ConsentOptions) => string
|
|
52
|
+
/**
|
|
53
|
+
* Withhold a loader until a stored or later decision grants tracking.
|
|
54
|
+
*
|
|
55
|
+
* The counterpart to `consentBootstrapScript` for the `'basic'` gated loading mode
|
|
56
|
+
* (`@owlmeans/web-gtm`'s `GOOGLE_TAG_DEFAULT_MODE`): where the bootstrap always declares Consent
|
|
57
|
+
* Mode's defaults so a tag may load cookieless, this withholds the tag itself — the loader string
|
|
58
|
+
* is only ever reached once `trackingGranted` is true, either because a returning visitor's stored
|
|
59
|
+
* record already says so, or because `applyConsent` later dispatches {@link CONSENT_EVENT} with
|
|
60
|
+
* one that does. The listener removes itself on the first passing event, so the loader — which
|
|
61
|
+
* marks its own element and refuses to run twice — is asked to run at most once from here.
|
|
62
|
+
*
|
|
63
|
+
* `loaderExpr` is a complete statement or expression that runs the loader when reached — the
|
|
64
|
+
* self-invoking IIFE string `googleTagHeadScript`'s `gtagScript`/`gtmContainerScript` already
|
|
65
|
+
* produce, embedded here verbatim rather than called. It is placed inline exactly as
|
|
66
|
+
* `consentBootstrapScript`'s own output is: dynamic values (the storage key, the category shape,
|
|
67
|
+
* the event name) are JSON-encoded so nothing configured can break out of a string literal, and
|
|
68
|
+
* escaping the RESULT for HTML (`</script`, `<!--`) is left to whoever composes the final inline
|
|
69
|
+
* script — the same discipline `consentBootstrapScript` follows, and `googleTagHeadScript` already
|
|
70
|
+
* applies to the whole script it stamps.
|
|
71
|
+
*/
|
|
72
|
+
consentGateScript: (loaderExpr: string, opts?: ConsentOptions & { categories?: ConsentCategory[] }) => string
|
|
73
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONSENT_AUTO_MAX_AGE, CONSENT_EVENT, CONSENT_KEY, CONSENT_SETUP_FLAG, CONSENT_SIGNAL_DEFAULTS,
|
|
3
|
+
DEFAULT_CONSENT_CATEGORIES,
|
|
4
|
+
} from './consts.js'
|
|
5
|
+
import { AUTO_SKEW } from './consts.local.js'
|
|
6
|
+
import { consentLinkHelper } from './linker.js'
|
|
7
|
+
import type { ConsentCategory, ConsentOptions, ConsentRecord } from './types.js'
|
|
8
|
+
import type { ConsentWindow } from './types.local.js'
|
|
9
|
+
import type { ConsentModeHelper } from './gtobserver/types.js'
|
|
10
|
+
|
|
11
|
+
export const createConsentModeHelper = (): ConsentModeHelper => {
|
|
12
|
+
const global = (): ConsentWindow | null =>
|
|
13
|
+
typeof window !== 'undefined' ? window as unknown as ConsentWindow : null
|
|
14
|
+
|
|
15
|
+
// A `function`, not an arrow: what goes on the queue is its own `arguments` object.
|
|
16
|
+
function gtagConsent(..._args: unknown[]): void {
|
|
17
|
+
const win = global()
|
|
18
|
+
if (win == null) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
win.dataLayer = win.dataLayer ?? []
|
|
22
|
+
// `arguments`, not `_args`. The rest parameter is only here to type the call sites; what goes on
|
|
23
|
+
// the queue must be the arguments object itself.
|
|
24
|
+
// eslint-disable-next-line prefer-rest-params
|
|
25
|
+
win.dataLayer.push(arguments)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const categoriesOf = (opts?: ConsentOptions): ConsentCategory[] =>
|
|
29
|
+
opts?.categories ?? DEFAULT_CONSENT_CATEGORIES
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Inline: treat an automatic decision past `CONSENT_AUTO_MAX_AGE` as no decision at all — run
|
|
33
|
+
* `onStale` on the parsed record `r`. The store holds such a record back until the visitor is
|
|
34
|
+
* located again, and the head scripts must not apply what the store will not.
|
|
35
|
+
*/
|
|
36
|
+
const staleAutoScript = (onStale: string): string =>
|
|
37
|
+
`if(r&&typeof r.auto==='number'){var a=Math.floor(Date.now()/1000)-r.auto;` +
|
|
38
|
+
`if(a>${CONSENT_AUTO_MAX_AGE}||a<-${AUTO_SKEW}){${onStale}}}`
|
|
39
|
+
|
|
40
|
+
const consentDefaults = (
|
|
41
|
+
categories?: ConsentCategory[]
|
|
42
|
+
): Record<string, 'granted' | 'denied'> => {
|
|
43
|
+
const used = new Set((categories ?? DEFAULT_CONSENT_CATEGORIES)
|
|
44
|
+
.flatMap(category => category.signals ?? []))
|
|
45
|
+
|
|
46
|
+
return Object.fromEntries(Object.entries(CONSENT_SIGNAL_DEFAULTS)
|
|
47
|
+
.filter(([signal]) => used.has(signal as never)))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const consentUpdate = (
|
|
51
|
+
record: ConsentRecord, categories?: ConsentCategory[]
|
|
52
|
+
): Record<string, 'granted' | 'denied'> => {
|
|
53
|
+
const update: Record<string, 'granted' | 'denied'> = {}
|
|
54
|
+
for (const category of categories ?? DEFAULT_CONSENT_CATEGORIES) {
|
|
55
|
+
const granted = category.required === true || record[category.key] === true
|
|
56
|
+
for (const signal of category.signals ?? []) {
|
|
57
|
+
// A signal named by two categories is granted only when every one of them is.
|
|
58
|
+
update[signal] = granted && update[signal] !== 'denied' ? 'granted' : 'denied'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return update
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const pushConsentDefaults = (opts?: ConsentOptions): void => {
|
|
66
|
+
const win = global()
|
|
67
|
+
if (win == null || opts?.silent === true || win[CONSENT_SETUP_FLAG] === true) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
gtagConsent('consent', 'default', consentDefaults(categoriesOf(opts)))
|
|
71
|
+
win[CONSENT_SETUP_FLAG] = true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const applyConsent = (record: ConsentRecord, opts?: ConsentOptions): void => {
|
|
75
|
+
const win = global()
|
|
76
|
+
if (win == null || opts?.silent === true) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
const categories = categoriesOf(opts)
|
|
80
|
+
|
|
81
|
+
for (const category of categories) {
|
|
82
|
+
if (category.globalVar != null) {
|
|
83
|
+
win[category.globalVar] = category.required === true || record[category.key] === true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
gtagConsent('consent', 'update', consentUpdate(record, categories))
|
|
88
|
+
|
|
89
|
+
for (const category of categories) {
|
|
90
|
+
if (category.event != null && (category.required === true || record[category.key] === true)) {
|
|
91
|
+
win.dataLayer = win.dataLayer ?? []
|
|
92
|
+
win.dataLayer.push({ event: category.event })
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A gate script (`consentGateScript`) that already decided NOT to load, because nothing was
|
|
97
|
+
// granted at the time it ran, has no other way to hear this. Consent Mode itself speaks only on
|
|
98
|
+
// `dataLayer`, which a tag that never loaded is not listening to.
|
|
99
|
+
if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function'
|
|
100
|
+
&& typeof CustomEvent === 'function') {
|
|
101
|
+
window.dispatchEvent(new CustomEvent(CONSENT_EVENT, { detail: { record } }))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const trackingGranted = (
|
|
106
|
+
record: ConsentRecord | null, categories: ConsentCategory[] = DEFAULT_CONSENT_CATEGORIES
|
|
107
|
+
): boolean => record != null && categories.some(
|
|
108
|
+
category => !category.required && (category.signals?.length ?? 0) > 0 && record[category.key] === true
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const consentBootstrapScript = (opts?: ConsentOptions): string => {
|
|
112
|
+
const defaults = JSON.stringify(consentDefaults(categoriesOf(opts)))
|
|
113
|
+
const categories = JSON.stringify(categoriesOf(opts).map(category => ({
|
|
114
|
+
key: category.key,
|
|
115
|
+
required: category.required === true,
|
|
116
|
+
signals: category.signals ?? [],
|
|
117
|
+
globalVar: category.globalVar ?? null,
|
|
118
|
+
})))
|
|
119
|
+
const storageKey = JSON.stringify(opts?.storageKey ?? CONSENT_KEY)
|
|
120
|
+
const flag = JSON.stringify(CONSENT_SETUP_FLAG)
|
|
121
|
+
// Right after the default is pushed and before EITHER storage read below (this one, and the
|
|
122
|
+
// `consentGateScript` that is concatenated after this whole IIFE) — an adopted decision must
|
|
123
|
+
// already be in storage by the time anything reads it, so `gtm.js`'s own `page_view` never sees
|
|
124
|
+
// the parameter and a granted decision loads the tag on arrival. A page with no `opts.linker`
|
|
125
|
+
// gets an empty string here, unchanged from before this existed.
|
|
126
|
+
const linker = opts?.linker != null ? `${consentLinkHelper.consentLinkerScript(opts)};` : ''
|
|
127
|
+
|
|
128
|
+
return `(function(w,d){` +
|
|
129
|
+
`w.dataLayer=w.dataLayer||[];function g(){w.dataLayer.push(arguments)}` +
|
|
130
|
+
`if(w[${flag}])return;g('consent','default',${defaults});w[${flag}]=true;` +
|
|
131
|
+
linker +
|
|
132
|
+
`var raw=null;try{raw=w.localStorage.getItem(${storageKey})}catch(e){}` +
|
|
133
|
+
`if(!raw){var p=('; '+d.cookie).split('; '+${storageKey}+'=');` +
|
|
134
|
+
`if(p.length===2){raw=p.pop().split(';').shift()}}` +
|
|
135
|
+
`if(!raw)return;var r;try{r=JSON.parse(raw)}catch(e){return}` +
|
|
136
|
+
staleAutoScript('return') +
|
|
137
|
+
`var cs=${categories},u={};` +
|
|
138
|
+
`for(var i=0;i<cs.length;i++){var c=cs[i];var ok=c.required||r[c.key]===true;` +
|
|
139
|
+
`if(c.globalVar){w[c.globalVar]=ok}` +
|
|
140
|
+
`for(var j=0;j<c.signals.length;j++){var s=c.signals[j];` +
|
|
141
|
+
`u[s]=ok&&u[s]!=='denied'?'granted':'denied'}}` +
|
|
142
|
+
`g('consent','update',u)})(window,document)`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const consentGateScript = (
|
|
146
|
+
loaderExpr: string, opts?: ConsentOptions & { categories?: ConsentCategory[] }
|
|
147
|
+
): string => {
|
|
148
|
+
const gate = JSON.stringify(categoriesOf(opts).map(category => ({
|
|
149
|
+
key: category.key,
|
|
150
|
+
required: category.required === true,
|
|
151
|
+
hasSignal: (category.signals ?? []).length > 0,
|
|
152
|
+
})))
|
|
153
|
+
const storageKey = JSON.stringify(opts?.storageKey ?? CONSENT_KEY)
|
|
154
|
+
const event = JSON.stringify(CONSENT_EVENT)
|
|
155
|
+
|
|
156
|
+
return `(function(w,d){` +
|
|
157
|
+
`function ok(r){if(!r)return false;var cs=${gate};` +
|
|
158
|
+
`for(var i=0;i<cs.length;i++){var c=cs[i];` +
|
|
159
|
+
`if(!c.required&&c.hasSignal&&r[c.key]===true)return true}return false}` +
|
|
160
|
+
`function load(){${loaderExpr}}` +
|
|
161
|
+
`var raw=null;try{raw=w.localStorage.getItem(${storageKey})}catch(e){}` +
|
|
162
|
+
`if(!raw){var p=('; '+d.cookie).split('; '+${storageKey}+'=');` +
|
|
163
|
+
`if(p.length===2){raw=p.pop().split(';').shift()}}` +
|
|
164
|
+
`var r=null;if(raw){try{r=JSON.parse(raw)}catch(e){r=null}}` +
|
|
165
|
+
staleAutoScript('r=null') +
|
|
166
|
+
`if(ok(r)){load();return}` +
|
|
167
|
+
`function h(ev){if(ok(ev&&ev.detail&&ev.detail.record)){` +
|
|
168
|
+
`w.removeEventListener(${event},h);load()}}` +
|
|
169
|
+
`w.addEventListener(${event},h)` +
|
|
170
|
+
`})(window,document)`
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
gtagConsent, consentDefaults, consentUpdate, pushConsentDefaults, applyConsent, trackingGranted,
|
|
175
|
+
consentBootstrapScript, consentGateScript,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const consentModeHelper = createConsentModeHelper()
|
|
180
|
+
|
|
181
|
+
/** @deprecated compat:factory-refactor — use `consentModeHelper.consentBootstrapScript(…)` */
|
|
182
|
+
export const consentBootstrapScript = (opts?: ConsentOptions): string => consentModeHelper.consentBootstrapScript(opts)
|
package/src/i18n/be.json
CHANGED
|
@@ -32,5 +32,12 @@
|
|
|
32
32
|
"privacy": "Палітыка прыватнасці",
|
|
33
33
|
"terms": "Умовы карыстання",
|
|
34
34
|
"domains": "Гэты выбар распаўсюджваецца на {{domains}}.",
|
|
35
|
-
"policyDomains": "Гэты выбар распаўсюджваецца на наступныя дамены:"
|
|
35
|
+
"policyDomains": "Гэты выбар распаўсюджваецца на наступныя дамены:",
|
|
36
|
+
"barTitle": "Файлы cookie на гэтым сайце",
|
|
37
|
+
"barDescription": "Мы выкарыстоўваем неабходныя файлы cookie для працы сайта — для ўваходу, забеспячэння бяспекі і запамінання вашага выбару. З вашай згоды мы таксама выкарыстоўвалі б наступныя неабавязковыя файлы cookie: {{categories}}. Яны застаюцца выключанымі, пакуль вы іх не дазволіце, а свой выбар вы можаце ў любы момант змяніць або адклікаць у наладах файлаў cookie.",
|
|
38
|
+
"barDescriptionEssential": "Гэты сайт выкарыстоўвае толькі неабходныя файлы cookie — для ўваходу, забеспячэння бяспекі і запамінання вашага выбару. Яны не патрабуюць згоды і не могуць быць адключаны.",
|
|
39
|
+
"acceptMandatory": "Прыняць толькі неабходныя",
|
|
40
|
+
"locating": "Вызначаем ваш рэгіён…",
|
|
41
|
+
"autoReason": "Гэтыя налады ўжыты аўтаматычна, бо там, адкуль вы праглядаеце сайт, згода на файлы cookie не патрабуецца. Вы можаце змяніць іх у любы момант.",
|
|
42
|
+
"policyRegional": "Там, дзе закон месца знаходжання наведвальніка патрабуе папярэдняй згоды, — у ЕС і ЕЭЗ, Вялікабрытаніі, Швейцарыі і іншых краінах з падобнымі правіламі — неабавязковыя файлы cookie застаюцца выключанымі, пакуль наведвальнік іх не дазволіць. У астатніх краінах яны ўключаны па змаўчанні, акрамя выпадкаў, калі браўзер перадае сігнал Global Privacy Control, які ўлічваецца як адмова; іх можна адключыць у любы момант кнопкай ніжэй."
|
|
36
43
|
}
|
package/src/i18n/de.json
CHANGED
|
@@ -32,5 +32,12 @@
|
|
|
32
32
|
"privacy": "Datenschutzerklärung",
|
|
33
33
|
"terms": "Nutzungsbedingungen",
|
|
34
34
|
"domains": "Diese Wahl gilt für {{domains}}.",
|
|
35
|
-
"policyDomains": "Diese Wahl gilt für die folgenden Domains:"
|
|
35
|
+
"policyDomains": "Diese Wahl gilt für die folgenden Domains:",
|
|
36
|
+
"barTitle": "Cookies auf dieser Website",
|
|
37
|
+
"barDescription": "Wir verwenden notwendige Cookies, damit diese Website funktioniert – für die Anmeldung, die Sicherheit und um Ihre Auswahl zu speichern. Mit Ihrer Einwilligung würden wir außerdem diese optionalen Cookies verwenden: {{categories}}. Sie bleiben deaktiviert, bis Sie sie erlauben, und Sie können Ihre Auswahl jederzeit in den Cookie-Einstellungen ändern oder widerrufen.",
|
|
38
|
+
"barDescriptionEssential": "Diese Website verwendet nur notwendige Cookies – für die Anmeldung, die Sicherheit und um Ihre Auswahl zu speichern. Sie erfordern keine Einwilligung und können nicht deaktiviert werden.",
|
|
39
|
+
"acceptMandatory": "Nur notwendige akzeptieren",
|
|
40
|
+
"locating": "Ihre Region wird ermittelt …",
|
|
41
|
+
"autoReason": "Diese Einstellungen wurden automatisch angewendet, weil dort, von wo aus Sie die Website aufrufen, keine Cookie-Einwilligung erforderlich ist. Sie können sie jederzeit ändern.",
|
|
42
|
+
"policyRegional": "Wo das Recht am Aufenthaltsort der Besucher eine vorherige Einwilligung verlangt – in der EU und im EWR, im Vereinigten Königreich, in der Schweiz und in weiteren Ländern mit ähnlichen Regeln –, bleiben optionale Cookies deaktiviert, bis sie erlaubt werden. Anderswo sind sie standardmäßig aktiviert, außer wenn der Browser ein Global-Privacy-Control-Signal sendet, das als Ablehnung beachtet wird; sie lassen sich jederzeit über die Schaltfläche unten deaktivieren."
|
|
36
43
|
}
|
package/src/i18n/en.json
CHANGED
|
@@ -32,5 +32,12 @@
|
|
|
32
32
|
"privacy": "Privacy Policy",
|
|
33
33
|
"terms": "Terms & Conditions",
|
|
34
34
|
"domains": "This choice applies to {{domains}}.",
|
|
35
|
-
"policyDomains": "This choice applies across these domains:"
|
|
35
|
+
"policyDomains": "This choice applies across these domains:",
|
|
36
|
+
"barTitle": "Cookies on this site",
|
|
37
|
+
"barDescription": "We use essential cookies to make this site work — for signing in, security and remembering your choices. With your consent we would also use these optional cookies: {{categories}}. They stay off until you allow them, and you can change or withdraw your choice at any time in the cookie preferences.",
|
|
38
|
+
"barDescriptionEssential": "This site uses only essential cookies — for signing in, security and remembering your choices. They need no consent and cannot be switched off.",
|
|
39
|
+
"acceptMandatory": "Accept only mandatory",
|
|
40
|
+
"locating": "Checking your region…",
|
|
41
|
+
"autoReason": "These settings were applied automatically, because cookie consent is not required where you are browsing from. You can change them at any time.",
|
|
42
|
+
"policyRegional": "Where the law of the visitor's location requires prior consent — the EU and EEA, the United Kingdom, Switzerland and other countries with similar rules — optional cookies stay off until the visitor allows them. Elsewhere they are switched on by default, except when the browser sends a Global Privacy Control signal, which is honoured as a refusal; they can be switched off at any time with the button below."
|
|
36
43
|
}
|
package/src/i18n/es.json
CHANGED
|
@@ -32,5 +32,12 @@
|
|
|
32
32
|
"privacy": "Política de privacidad",
|
|
33
33
|
"terms": "Términos y condiciones",
|
|
34
34
|
"domains": "Esta elección se aplica a {{domains}}.",
|
|
35
|
-
"policyDomains": "Esta elección se aplica a los siguientes dominios:"
|
|
35
|
+
"policyDomains": "Esta elección se aplica a los siguientes dominios:",
|
|
36
|
+
"barTitle": "Cookies en este sitio",
|
|
37
|
+
"barDescription": "Usamos cookies esenciales para que este sitio funcione: para iniciar sesión, por seguridad y para recordar tus elecciones. Con tu consentimiento también usaríamos estas cookies opcionales: {{categories}}. Permanecen desactivadas hasta que las permitas, y puedes cambiar o retirar tu elección en cualquier momento en las preferencias de cookies.",
|
|
38
|
+
"barDescriptionEssential": "Este sitio solo usa cookies esenciales: para iniciar sesión, por seguridad y para recordar tus elecciones. No requieren consentimiento y no se pueden desactivar.",
|
|
39
|
+
"acceptMandatory": "Aceptar solo las esenciales",
|
|
40
|
+
"locating": "Comprobando tu región…",
|
|
41
|
+
"autoReason": "Esta configuración se aplicó automáticamente porque, desde donde navegas, no se requiere consentimiento para las cookies. Puedes cambiarla en cualquier momento.",
|
|
42
|
+
"policyRegional": "Donde la ley del lugar en que se encuentra el visitante exige consentimiento previo —la UE y el EEE, el Reino Unido, Suiza y otros países con normas similares—, las cookies opcionales permanecen desactivadas hasta que el visitante las permita. En el resto están activadas por defecto, salvo cuando el navegador envía una señal Global Privacy Control, que se respeta como rechazo; pueden desactivarse en cualquier momento con el botón de abajo."
|
|
36
43
|
}
|
package/src/i18n/fr.json
CHANGED
|
@@ -32,5 +32,12 @@
|
|
|
32
32
|
"privacy": "Politique de confidentialité",
|
|
33
33
|
"terms": "Conditions générales",
|
|
34
34
|
"domains": "Ce choix s'applique à {{domains}}.",
|
|
35
|
-
"policyDomains": "Ce choix s'applique aux domaines suivants :"
|
|
35
|
+
"policyDomains": "Ce choix s'applique aux domaines suivants :",
|
|
36
|
+
"barTitle": "Cookies sur ce site",
|
|
37
|
+
"barDescription": "Nous utilisons des cookies essentiels au fonctionnement de ce site : pour la connexion, la sécurité et la mémorisation de vos choix. Avec votre consentement, nous utiliserions également ces cookies facultatifs : {{categories}}. Ils restent désactivés tant que vous ne les autorisez pas, et vous pouvez modifier ou retirer votre choix à tout moment dans les préférences de cookies.",
|
|
38
|
+
"barDescriptionEssential": "Ce site n'utilise que des cookies essentiels : pour la connexion, la sécurité et la mémorisation de vos choix. Ils ne nécessitent pas de consentement et ne peuvent pas être désactivés.",
|
|
39
|
+
"acceptMandatory": "Accepter uniquement les essentiels",
|
|
40
|
+
"locating": "Vérification de votre région…",
|
|
41
|
+
"autoReason": "Ces paramètres ont été appliqués automatiquement, car le consentement aux cookies n'est pas requis là où vous naviguez. Vous pouvez les modifier à tout moment.",
|
|
42
|
+
"policyRegional": "Là où la loi du lieu où se trouve le visiteur exige un consentement préalable — l'UE et l'EEE, le Royaume-Uni, la Suisse et d'autres pays aux règles similaires —, les cookies facultatifs restent désactivés tant que le visiteur ne les autorise pas. Ailleurs, ils sont activés par défaut, sauf lorsque le navigateur envoie un signal Global Privacy Control, respecté comme un refus ; ils peuvent être désactivés à tout moment avec le bouton ci-dessous."
|
|
36
43
|
}
|