@oslokommune/punkt-react 16.21.0 → 16.21.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oslokommune/punkt-react",
3
- "version": "16.21.0",
3
+ "version": "16.21.1",
4
4
  "description": "React komponentbibliotek til Punkt, et designsystem laget av Oslo Origo",
5
5
  "homepage": "https://punkt.oslo.kommune.no",
6
6
  "author": "Team Designsystem, Oslo Origo",
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@lit-labs/ssr-dom-shim": "^1.2.1",
41
41
  "@lit/react": "^1.0.7",
42
- "@oslokommune/punkt-elements": "^16.21.0",
42
+ "@oslokommune/punkt-elements": "^16.21.1",
43
43
  "classnames": "^2.5.1",
44
44
  "prettier": "^3.3.3",
45
45
  "react-hook-form": "^7.53.0"
@@ -109,5 +109,5 @@
109
109
  "url": "https://github.com/oslokommune/punkt/issues"
110
110
  },
111
111
  "license": "MIT",
112
- "gitHead": "f7f1779ccee5d510e6d011e7ec3edba89b41c255"
112
+ "gitHead": "15799459d01ea67c7770b8603da84069a717a164"
113
113
  }
@@ -0,0 +1,217 @@
1
+ import '@testing-library/jest-dom'
2
+
3
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
4
+ import { axe, toHaveNoViolations } from 'jest-axe'
5
+ import { afterEach, beforeEach, vi } from 'vitest'
6
+
7
+ import { PktConsent } from './Consent'
8
+
9
+ expect.extend(toHaveNoViolations)
10
+
11
+ type CapturedHandler = ((consent: { value: string | { items: unknown[] } }) => void) | undefined
12
+ let capturedHandler: CapturedHandler
13
+
14
+ const installCookieBannerStub = ({ hasConsent = false }: { hasConsent?: boolean } = {}) => {
15
+ Object.defineProperty(window, 'cookieBanner', {
16
+ value: {
17
+ cookieConsent: {
18
+ validateConsentCookie: vi.fn().mockResolvedValue(hasConsent),
19
+ getConsentCookie: vi.fn().mockReturnValue('{"items":[]}'),
20
+ },
21
+ openCookieModal: vi.fn(),
22
+ },
23
+ writable: true,
24
+ configurable: true,
25
+ })
26
+ capturedHandler = undefined
27
+ Object.defineProperty(window, '__cookieEvents', {
28
+ value: {
29
+ on: vi.fn((_event: string, handler: CapturedHandler) => {
30
+ capturedHandler = handler
31
+ }),
32
+ off: vi.fn(),
33
+ },
34
+ writable: true,
35
+ configurable: true,
36
+ })
37
+ }
38
+
39
+ const preInjectScriptMarker = () => {
40
+ // The component's loadConsentScript() resolves immediately if the marker
41
+ // script is already in the DOM. Inject it up-front so the test doesn't
42
+ // depend on a network fetch that jsdom can't fulfil.
43
+ if (!document.querySelector('#oslo-consent-script')) {
44
+ const marker = document.createElement('script')
45
+ marker.id = 'oslo-consent-script'
46
+ document.head.appendChild(marker)
47
+ }
48
+ }
49
+
50
+ describe('PktConsent', () => {
51
+ beforeEach(() => {
52
+ preInjectScriptMarker()
53
+ installCookieBannerStub()
54
+ })
55
+
56
+ afterEach(() => {
57
+ document.querySelector('#oslo-consent-script')?.remove()
58
+ document.querySelector('#oslo-consent-styles')?.remove()
59
+ delete (window as { cookieBanner?: unknown }).cookieBanner
60
+ delete (window as { __cookieEvents?: unknown }).__cookieEvents
61
+ capturedHandler = undefined
62
+ })
63
+
64
+ describe('Trigger rendering', () => {
65
+ test('renders the default button trigger with the bokmål settings label', () => {
66
+ render(<PktConsent />)
67
+
68
+ expect(
69
+ screen.getByRole('button', { name: 'Innstillinger for informasjonskapsler' }),
70
+ ).toBeInTheDocument()
71
+ })
72
+
73
+ test('uses the English settings label when i18nLanguage is "en"', () => {
74
+ render(<PktConsent i18nLanguage="en" />)
75
+
76
+ expect(screen.getByRole('button', { name: 'Go to settings' })).toBeInTheDocument()
77
+ })
78
+
79
+ test('renders a custom triggerText when provided', () => {
80
+ render(<PktConsent triggerText="Endre samtykke" />)
81
+
82
+ expect(screen.getByRole('button', { name: 'Endre samtykke' })).toBeInTheDocument()
83
+ })
84
+
85
+ test('renders a link trigger when triggerType="link"', () => {
86
+ const { container } = render(<PktConsent triggerType="link" triggerText="Innstillinger" />)
87
+
88
+ const link = container.querySelector('a.pkt-link')
89
+ expect(link).toBeInTheDocument()
90
+ expect(link).toHaveAttribute('href', '#')
91
+ expect(link).toHaveTextContent('Innstillinger')
92
+ })
93
+
94
+ test('renders a footer link with chevron icon when triggerType="footerlink"', () => {
95
+ const { container } = render(
96
+ <PktConsent triggerType="footerlink" triggerText="Innstillinger" />,
97
+ )
98
+
99
+ const link = container.querySelector('a.pkt-footer__link')
100
+ expect(link).toBeInTheDocument()
101
+ expect(link?.querySelector('.pkt-icon')).toHaveAttribute('name', 'chevron-right')
102
+ })
103
+
104
+ test('renders an icon-only button when triggerType="icon"', () => {
105
+ const { container } = render(<PktConsent triggerType="icon" triggerText="Cookies" />)
106
+
107
+ const button = container.querySelector('button')
108
+ expect(button).toHaveClass('pkt-btn--icon-only')
109
+ expect(button).toHaveClass('pkt-btn--tertiary')
110
+ })
111
+ })
112
+
113
+ describe('Click behaviour', () => {
114
+ test('opens the cookie modal on button click', () => {
115
+ vi.useFakeTimers()
116
+ try {
117
+ render(<PktConsent />)
118
+ fireEvent.click(screen.getByRole('button'))
119
+ vi.runAllTimers()
120
+ expect(window.cookieBanner?.openCookieModal).toHaveBeenCalled()
121
+ } finally {
122
+ vi.useRealTimers()
123
+ }
124
+ })
125
+
126
+ test('opens the cookie modal on link click', () => {
127
+ vi.useFakeTimers()
128
+ try {
129
+ const { container } = render(<PktConsent triggerType="link" />)
130
+ const link = container.querySelector('a.pkt-link') as HTMLAnchorElement
131
+ fireEvent.click(link)
132
+ vi.runAllTimers()
133
+ expect(window.cookieBanner?.openCookieModal).toHaveBeenCalled()
134
+ } finally {
135
+ vi.useRealTimers()
136
+ }
137
+ })
138
+ })
139
+
140
+ describe('Side effects', () => {
141
+ test('sets the cookie banner globals from props', async () => {
142
+ render(
143
+ <PktConsent
144
+ devMode
145
+ googleAnalyticsId="GA-123"
146
+ hotjarId="HJ-456"
147
+ cookieDomain=".example.com"
148
+ cookieSecure="true"
149
+ cookieExpiryDays="90"
150
+ />,
151
+ )
152
+
153
+ await waitFor(() => {
154
+ expect(window.cookieBanner_googleAnalyticsId).toBe('GA-123')
155
+ })
156
+ expect(window.cookieBanner_hotjarId).toBe('HJ-456')
157
+ expect(window.cookieBanner_cookieDomain).toBe('.example.com')
158
+ expect(window.cookieBanner_cookieSecure).toBe('true')
159
+ expect(window.cookieBanner_cookieExpiryDays).toBe('90')
160
+ expect(window.cookieBanner_devMode).toBe(true)
161
+ })
162
+ })
163
+
164
+ describe('onToggleConsent', () => {
165
+ test('invokes onToggleConsent with a CustomEvent carrying parsed consent details', async () => {
166
+ installCookieBannerStub({ hasConsent: true })
167
+ const onToggle = vi.fn()
168
+
169
+ render(<PktConsent onToggleConsent={onToggle} />)
170
+
171
+ // First emit comes from the initial consent-cookie read; wait for it,
172
+ // then clear so we can assert specifically on the handler-driven call.
173
+ await waitFor(() => expect(capturedHandler).toBeDefined())
174
+ await waitFor(() => expect(onToggle).toHaveBeenCalled())
175
+ onToggle.mockClear()
176
+
177
+ capturedHandler!({
178
+ value: JSON.stringify({
179
+ items: [
180
+ { name: 'statistics', consent: true },
181
+ { name: 'survey', consent: false },
182
+ ],
183
+ }),
184
+ })
185
+
186
+ expect(onToggle).toHaveBeenCalledTimes(1)
187
+ const event = onToggle.mock.calls[0][0] as CustomEvent<Record<string, boolean>>
188
+ expect(event).toBeInstanceOf(CustomEvent)
189
+ expect(event.type).toBe('toggle-consent')
190
+ expect(event.detail).toEqual({ statistics: true, survey: false })
191
+ })
192
+ })
193
+
194
+ describe('Accessibility', () => {
195
+ test('button trigger has no axe violations', async () => {
196
+ const { container } = render(<PktConsent triggerText="Cookie settings" />)
197
+ const results = await axe(container)
198
+ expect(results).toHaveNoViolations()
199
+ })
200
+
201
+ test('link trigger has no axe violations', async () => {
202
+ const { container } = render(
203
+ <PktConsent triggerType="link" triggerText="Cookie settings" />,
204
+ )
205
+ const results = await axe(container)
206
+ expect(results).toHaveNoViolations()
207
+ })
208
+
209
+ test('footer link trigger has no axe violations', async () => {
210
+ const { container } = render(
211
+ <PktConsent triggerType="footerlink" triggerText="Cookie settings" />,
212
+ )
213
+ const results = await axe(container)
214
+ expect(results).toHaveNoViolations()
215
+ })
216
+ })
217
+ })
@@ -1,23 +1,282 @@
1
1
  'use client'
2
2
 
3
- import { createComponent, EventName } from '@lit/react'
4
- import { type IPktConsent as IPktElConsent, PktConsent as PktElConsent } from '@oslokommune/punkt-elements'
5
- // eslint-disable-next-line no-restricted-syntax -- React is required for createComponent
6
- import React, { ForwardRefExoticComponent, LegacyRef } from 'react'
7
-
8
- export interface IPktConsent extends IPktElConsent {
9
- ref?: LegacyRef<HTMLDivElement>
10
- onToggleConsent?: (e: CustomEvent) => void
11
- }
12
-
13
- export const PktConsent: ForwardRefExoticComponent<IPktConsent> = createComponent({
14
- tagName: 'pkt-consent',
15
- elementClass: PktElConsent,
16
- react: React,
17
- displayName: 'PktConsent',
18
- events: {
19
- onToggleConsent: 'toggle-consent' as EventName<CustomEvent>,
3
+ import {
4
+ forwardRef,
5
+ HTMLAttributes,
6
+ MouseEvent as ReactMouseEvent,
7
+ Ref,
8
+ useEffect,
9
+ useRef,
10
+ } from 'react'
11
+
12
+ import { PktButton } from '../button/Button'
13
+ import { PktIcon } from '../icon/Icon'
14
+ import { consentStrings } from './strings'
15
+
16
+ interface ConsentItem {
17
+ name: string
18
+ consent: boolean
19
+ }
20
+
21
+ interface ConsentValue {
22
+ items: ConsentItem[]
23
+ }
24
+
25
+ interface CookieConsentEvent {
26
+ value: string | ConsentValue
27
+ }
28
+
29
+ type ConsentDetailsMap = Record<string, boolean>
30
+
31
+ type CookieEventHandler = (consent: CookieConsentEvent) => void
32
+
33
+ interface CookieEvents {
34
+ on(eventName: string, handler: CookieEventHandler): void
35
+ off(eventName: string, handler: CookieEventHandler): void
36
+ }
37
+
38
+ interface CookieBannerApi {
39
+ cookieConsent: {
40
+ validateConsentCookie(): Promise<boolean>
41
+ getConsentCookie(): string | ConsentValue
42
+ }
43
+ openCookieModal(): void
44
+ }
45
+
46
+ declare global {
47
+ interface Window {
48
+ cookieBanner_devMode?: boolean
49
+ cookieBanner_cookieDomain?: string | null
50
+ cookieBanner_cookieSecure?: string | null
51
+ cookieBanner_cookieExpiryDays?: string | null
52
+ cookieBanner_googleAnalyticsId?: string | null
53
+ cookieBanner_hotjarId?: string | null
54
+ cookieBanner?: CookieBannerApi
55
+ __cookieEvents?: CookieEvents
56
+ }
57
+ }
58
+
59
+ let consentScriptPromise: Promise<void> | null = null
60
+
61
+ const loadConsentScript = (): Promise<void> => {
62
+ if (consentScriptPromise) return consentScriptPromise
63
+
64
+ consentScriptPromise = new Promise((resolve, reject) => {
65
+ if (document.querySelector('#oslo-consent-script')) {
66
+ resolve()
67
+ return
68
+ }
69
+ const script = document.createElement('script')
70
+ script.src = 'https://cdn.web.oslo.kommune.no/cb/cb-v1.1.0.js'
71
+ script.id = 'oslo-consent-script'
72
+ script.onload = () => resolve()
73
+ script.onerror = reject
74
+ document.head.appendChild(script)
75
+
76
+ const styles = document.createElement('link')
77
+ styles.href = 'https://cdn.web.oslo.kommune.no/cb/cb-v1.1.0.css'
78
+ styles.type = 'text/css'
79
+ styles.rel = 'stylesheet'
80
+ styles.id = 'oslo-consent-styles'
81
+ document.head.appendChild(styles)
82
+ })
83
+
84
+ return consentScriptPromise
85
+ }
86
+
87
+ const parseJsonOrPassThrough = <T,>(value: T | string): T => {
88
+ if (typeof value !== 'string') return value
89
+ try {
90
+ return JSON.parse(value) as T
91
+ } catch {
92
+ return value as unknown as T
93
+ }
94
+ }
95
+
96
+ const toConsentDetails = (consent: CookieConsentEvent): ConsentDetailsMap => {
97
+ const parsed = parseJsonOrPassThrough<ConsentValue>(consent.value)
98
+ return parsed.items.reduce<ConsentDetailsMap>((acc, item) => {
99
+ acc[item.name] = item.consent
100
+ return acc
101
+ }, {})
102
+ }
103
+
104
+ export type TPktConsentTriggerType = 'button' | 'link' | 'footerlink' | 'icon'
105
+
106
+ export interface IPktConsent extends HTMLAttributes<HTMLElement> {
107
+ devMode?: boolean
108
+ cookieDomain?: string | null
109
+ cookieSecure?: string | null
110
+ cookieExpiryDays?: string | null
111
+ hotjarId?: string | null
112
+ googleAnalyticsId?: string | null
113
+ i18nLanguage?: string
114
+ triggerType?: TPktConsentTriggerType | null
115
+ triggerText?: string | null
116
+ onToggleConsent?: (e: CustomEvent<ConsentDetailsMap>) => void
117
+ }
118
+
119
+ export const PktConsent = forwardRef<HTMLElement, IPktConsent>(function PktConsent(
120
+ {
121
+ devMode = false,
122
+ cookieDomain = null,
123
+ cookieSecure = null,
124
+ cookieExpiryDays = null,
125
+ hotjarId = null,
126
+ googleAnalyticsId = null,
127
+ i18nLanguage = 'nb',
128
+ triggerType = 'button',
129
+ triggerText,
130
+ onToggleConsent,
131
+ ...rest
20
132
  },
21
- }) as ForwardRefExoticComponent<IPktConsent>
133
+ ref,
134
+ ) {
135
+ const onToggleRef = useRef(onToggleConsent)
136
+ onToggleRef.current = onToggleConsent
137
+
138
+ const configRef = useRef({
139
+ devMode,
140
+ cookieDomain,
141
+ cookieSecure,
142
+ cookieExpiryDays,
143
+ hotjarId,
144
+ googleAnalyticsId,
145
+ })
146
+ configRef.current = {
147
+ devMode,
148
+ cookieDomain,
149
+ cookieSecure,
150
+ cookieExpiryDays,
151
+ hotjarId,
152
+ googleAnalyticsId,
153
+ }
154
+
155
+ const triggerInitRef = useRef<() => void>(() => {})
156
+
157
+ const language = (i18nLanguage in consentStrings.i18n
158
+ ? i18nLanguage
159
+ : 'nb') as keyof typeof consentStrings.i18n
160
+ const resolvedTriggerText =
161
+ triggerText ?? consentStrings.i18n[language].contentPresentation.buttons.settings
162
+
163
+ useEffect(() => {
164
+ let cancelled = false
165
+ let cookieEventHandler: CookieEventHandler | undefined
166
+
167
+ const config = configRef.current
168
+ // cookieBanner_* identifiers are defined by the external cookie banner script
169
+ /* eslint-disable camelcase */
170
+ window.cookieBanner_googleAnalyticsId = config.googleAnalyticsId
171
+ window.cookieBanner_hotjarId = config.hotjarId
172
+ if (config.cookieDomain) window.cookieBanner_cookieDomain = config.cookieDomain
173
+ if (config.cookieSecure) window.cookieBanner_cookieSecure = config.cookieSecure
174
+ if (config.cookieExpiryDays) window.cookieBanner_cookieExpiryDays = config.cookieExpiryDays
175
+ if (config.devMode) window.cookieBanner_devMode = config.devMode
176
+ /* eslint-enable camelcase */
177
+
178
+ const emitConsents = (consent: CookieConsentEvent) => {
179
+ const handler = onToggleRef.current
180
+ if (!handler) return
181
+ handler(
182
+ new CustomEvent<ConsentDetailsMap>('toggle-consent', {
183
+ detail: toConsentDetails(consent),
184
+ bubbles: true,
185
+ cancelable: false,
186
+ }),
187
+ )
188
+ }
189
+
190
+ triggerInitRef.current = () => {
191
+ window.document.dispatchEvent(
192
+ new Event('CookieBannerReady', { bubbles: true, cancelable: true }),
193
+ )
194
+
195
+ const banner = window.cookieBanner
196
+ const events = window.__cookieEvents
197
+ if (!banner || !events) return
198
+
199
+ banner.cookieConsent.validateConsentCookie().then((hasConsent) => {
200
+ if (cancelled || !hasConsent) return
201
+
202
+ const cookie = banner.cookieConsent.getConsentCookie()
203
+ window.setTimeout(() => {
204
+ if (!cancelled) emitConsents({ value: cookie })
205
+ }, 0)
206
+
207
+ if (cookieEventHandler) {
208
+ events.off('CookieManager.setCookie', cookieEventHandler)
209
+ }
210
+ cookieEventHandler = (consent) => emitConsents(consent)
211
+ events.on('CookieManager.setCookie', cookieEventHandler)
212
+ })
213
+ }
214
+
215
+ loadConsentScript().then(() => {
216
+ if (!cancelled) triggerInitRef.current()
217
+ })
218
+
219
+ return () => {
220
+ cancelled = true
221
+ if (cookieEventHandler) {
222
+ window.__cookieEvents?.off('CookieManager.setCookie', cookieEventHandler)
223
+ }
224
+ }
225
+ }, [])
226
+
227
+ const openModal = (e: ReactMouseEvent<HTMLElement>) => {
228
+ e.preventDefault()
229
+ if (!window.cookieBanner?.cookieConsent) {
230
+ triggerInitRef.current()
231
+ }
232
+ setTimeout(() => window.cookieBanner?.openCookieModal())
233
+ }
234
+
235
+ if (triggerType === 'link') {
236
+ return (
237
+ <a
238
+ {...rest}
239
+ ref={ref as Ref<HTMLAnchorElement>}
240
+ href="#"
241
+ className="pkt-link"
242
+ onClick={openModal}
243
+ >
244
+ {resolvedTriggerText}
245
+ </a>
246
+ )
247
+ }
248
+ if (triggerType === 'footerlink') {
249
+ return (
250
+ <a
251
+ {...rest}
252
+ ref={ref as Ref<HTMLAnchorElement>}
253
+ href="#"
254
+ className="pkt-footer__link"
255
+ onClick={openModal}
256
+ >
257
+ <PktIcon name="chevron-right" className="pkt-footer__link-icon" />
258
+ {resolvedTriggerText}
259
+ </a>
260
+ )
261
+ }
262
+ if (triggerType === 'icon') {
263
+ return (
264
+ <PktButton
265
+ ref={ref as Ref<HTMLButtonElement>}
266
+ skin="tertiary"
267
+ variant="icon-only"
268
+ iconName="cookie"
269
+ onClick={openModal}
270
+ >
271
+ {resolvedTriggerText}
272
+ </PktButton>
273
+ )
274
+ }
275
+ return (
276
+ <PktButton ref={ref as Ref<HTMLButtonElement>} onClick={openModal}>
277
+ {resolvedTriggerText}
278
+ </PktButton>
279
+ )
280
+ })
22
281
 
23
282
  PktConsent.displayName = 'PktConsent'
@@ -0,0 +1,125 @@
1
+ export const consentStrings = {
2
+ i18n: {
3
+ nb: {
4
+ contentPresentation: {
5
+ title: 'Oslo kommune bruker informasjonskapsler',
6
+ description: [
7
+ 'For at nettstedet skal fungere og være trygt, bruker Oslo kommune informasjonskapsler. Noen er teknisk nødvendige, mens andre sikrer ulik funksjonalitet.',
8
+ 'Godtar du alle informasjonskapsler, tillater du også at vi samler inn data om statistikk og brukeradferd. Da hjelper du oss med å lage et bedre nettsted uten at du trenger å dele noe personlig informasjon med oss.',
9
+ ],
10
+ buttons: {
11
+ accept: 'Godta alle',
12
+ reject: 'Kun nødvendige',
13
+ settings: 'Innstillinger for informasjonskapsler',
14
+ },
15
+ },
16
+ contentSettings: {
17
+ title: 'Innstillinger for informasjonskapsler',
18
+ description: [
19
+ 'Her kan du velge hvilke typer informasjonskapsler du vil tillate. Tillatelsen gjelder i 90 dager. Husk at nødvendige informasjonskapsler ikke kan velges bort.',
20
+ 'Du kan når som helst endre innstillingene og finne mer informasjon nederst på nettstedet under «Innstillinger for informasjonskapsler» og «Personvern og informasjonskapsler».',
21
+ ],
22
+ buttons: {
23
+ back: 'Tilbake',
24
+ save: 'Lagre innstillinger',
25
+ },
26
+ },
27
+ },
28
+ en: {
29
+ contentPresentation: {
30
+ title: 'Before you visit Oslo kommune ...',
31
+ description: [
32
+ 'This website uses cookies to make improvements. In this context, we need your consent to measure the traffic on the website in relation to statistics and feedback.',
33
+ 'To read more about what we use cookies for, go to our privacy declaration which you will find at the bottom of our websites.',
34
+ ],
35
+ buttons: {
36
+ accept: 'Yes, I accept',
37
+ reject: 'Only necessary',
38
+ settings: 'Go to settings',
39
+ },
40
+ },
41
+ contentSettings: {
42
+ title: 'Her kan du aktivt velge mellom ulike informasjonskapsler',
43
+ description: [
44
+ 'For å lese mer om hva vi bruker informasjonskapsler til gå til vår personvernserklering som du finner på våre nettsider',
45
+ ],
46
+ buttons: {
47
+ back: 'Back',
48
+ save: 'Save settings',
49
+ },
50
+ },
51
+ },
52
+ },
53
+ cookies: [
54
+ {
55
+ name: 'statistics',
56
+ defaultValue: null,
57
+ i18n: {
58
+ nb: {
59
+ title: 'Statistikk og analyse',
60
+ description:
61
+ 'Jeg godtar at Oslo kommune bruker informasjonskapsler til å innhente data om statistikk og brukeradferd.',
62
+ buttons: {
63
+ yes: 'Ja',
64
+ no: 'Nei',
65
+ },
66
+ },
67
+ en: {
68
+ title: 'Statistics and analytics',
69
+ description: 'Select your preference for statistics and analytics',
70
+ buttons: {
71
+ yes: 'Yes',
72
+ no: 'No',
73
+ },
74
+ },
75
+ },
76
+ },
77
+ {
78
+ name: 'survey',
79
+ defaultValue: null,
80
+ i18n: {
81
+ nb: {
82
+ title: 'Skriftlige tilbakemeldinger',
83
+ description:
84
+ 'Jeg godtar at Oslo kommune bruker informasjonskapsler til å vise responsfelt og registrere tilbakemeldinger.',
85
+ buttons: {
86
+ yes: 'Ja',
87
+ no: 'Nei',
88
+ },
89
+ },
90
+ en: {
91
+ title: 'Survey',
92
+ description: 'Select your preference for survey',
93
+ buttons: {
94
+ yes: 'Yes',
95
+ no: 'No',
96
+ },
97
+ },
98
+ },
99
+ },
100
+ {
101
+ name: 'functional',
102
+ defaultValue: true,
103
+ i18n: {
104
+ nb: {
105
+ title: 'Funksjonelle informasjonskapsler',
106
+ description:
107
+ 'Jeg godtar at Oslo kommune bruker informasjonskapsler for å gjøre ulike funksjoner på nettstedet mulige (for eksempel skjemaer og barnehagevelger).',
108
+ buttons: {
109
+ yes: 'Ja',
110
+ no: 'Nei',
111
+ },
112
+ },
113
+ en: {
114
+ title: 'Functional cookies',
115
+ description:
116
+ 'For the website to function optimally, we recommend that you accept these cookies',
117
+ buttons: {
118
+ yes: 'Yes',
119
+ no: 'No',
120
+ },
121
+ },
122
+ },
123
+ },
124
+ ],
125
+ }