@owlmeans/web-panel 0.1.18-rc.61 → 0.1.18-rc.62

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
@@ -11,7 +11,7 @@ but never started.
11
11
  ## Installation
12
12
 
13
13
  ```sh
14
- bun add @owlmeans/web-panel@^0.1.18-rc.61
14
+ bun add @owlmeans/web-panel@^0.1.18-rc.62
15
15
  ```
16
16
 
17
17
  Peer requirements (the consuming app provides these): `react`, `react-dom`,
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/web-panel",
4
- "version": "0.1.18-rc.61",
5
- "generatedAt": "2026-09-24T14:00:35.085Z",
4
+ "version": "0.1.18-rc.62",
5
+ "generatedAt": "2026-09-25T15:32:39.588Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/web-panel
9
9
 
10
10
  **Layer:** Web (React)
11
- **Install:** `"@owlmeans/web-panel": "^0.1.18-rc.61"` in `dependencies`
11
+ **Install:** `"@owlmeans/web-panel": "^0.1.18-rc.62"` in `dependencies`
12
12
 
13
13
  ## Key Exports
14
14
 
@@ -499,6 +499,7 @@ menu has already mounted) and because React 18 StrictMode double-invokes mount/c
499
499
  | Job | Import from `@owlmeans/web-panel/consent` | Where it goes |
500
500
  |---|---|---|
501
501
  | The consent dialog (and its floating re-open button) | `PanelCookieConsent` | Beside the router — a `PanelApp` child — once |
502
+ | Remember the language only while the visitor granted `functional` cookies | `installConsentLanguage()` | Once, in the bootstrap, BEFORE `prepareI18n` — wires `client-i18n`'s persistence guard, and applies a language that arrived with a link from another OwlMeans domain once the grant is saved |
502
503
  | The "Cookie settings" control in a footer or menu | `PanelConsentMenuWidget` (`label`, `className`, `onSelect?` — defaults to `openConsent('reopen')`) | Inside the host's own footer/menu |
503
504
  | Hiding the floating button while that control is reachable | `useConsentMenuPresence()` | Called by the always-mounted component that renders the control |
504
505
  | Registering the presence service | `appendConsentWidgetService(context)` | The app's `context.ts` |
@@ -1,9 +1,10 @@
1
1
  export * from './component.js';
2
2
  export * from './menu-widget.js';
3
3
  export * from './presence.js';
4
+ export { installConsentLanguage } from './language.js';
4
5
  export { createConsentWidgetService, appendConsentWidgetService, CONSENT_WIDGET_STATE } from './service.js';
5
6
  export { CONSENT_WIDGET_SERVICE } from './consts.js';
6
7
  export type { ConsentWidgetPresenceRecord, ConsentWidgetService, ConsentWidgetServiceAppend } from './types.js';
7
- export { useConsent, useConsentCategory, consentStore, openConsent, isConsented, readConsent, writeConsent, clearConsent, DEFAULT_CONSENT_CATEGORIES, CONSENT_KEY, CONSENT_ESSENTIAL, CONSENT_ANALYTICS, CONSENT_MARKETING, consentBootstrapScript, } from '@owlmeans/web-consent';
8
+ export { useConsent, useConsentCategory, consentStore, openConsent, isConsented, readConsent, writeConsent, clearConsent, DEFAULT_CONSENT_CATEGORIES, CONSENT_KEY, CONSENT_ESSENTIAL, CONSENT_FUNCTIONAL, CONSENT_ANALYTICS, CONSENT_MARKETING, CONSENT_EVENT, CONSENT_LANGUAGE_EVENT, functionalGranted, consentBootstrapScript, } from '@owlmeans/web-consent';
8
9
  export type { ConsentCategory, ConsentMenuWidgetProps, ConsentOptions, ConsentRecord, ConsentSignal, ConsentState, CookieConsentProps, CookiePolicyProps, } from '@owlmeans/web-consent';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/consent/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,eAAe,CAAA;AAC7B,OAAO,EAAE,0BAA0B,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAC3G,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AACpD,YAAY,EACV,2BAA2B,EAAE,oBAAoB,EAAE,0BAA0B,EAC9E,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,UAAU,EAAE,kBAAkB,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EACtE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,0BAA0B,EACnE,WAAW,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,EACpE,sBAAsB,GACvB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EAAE,sBAAsB,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EACrF,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,GACpD,MAAM,uBAAuB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/consent/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,eAAe,CAAA;AAC7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AACtD,OAAO,EAAE,0BAA0B,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAC3G,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AACpD,YAAY,EACV,2BAA2B,EAAE,oBAAoB,EAAE,0BAA0B,EAC9E,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,UAAU,EAAE,kBAAkB,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EACtE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,0BAA0B,EACnE,WAAW,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,iBAAiB,EACxF,aAAa,EAAE,sBAAsB,EAAE,iBAAiB,EACxD,sBAAsB,GACvB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EAAE,sBAAsB,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EACrF,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,GACpD,MAAM,uBAAuB,CAAA"}
@@ -1,7 +1,8 @@
1
1
  export * from './component.js';
2
2
  export * from './menu-widget.js';
3
3
  export * from './presence.js';
4
+ export { installConsentLanguage } from './language.js';
4
5
  export { createConsentWidgetService, appendConsentWidgetService, CONSENT_WIDGET_STATE } from './service.js';
5
6
  export { CONSENT_WIDGET_SERVICE } from './consts.js';
6
- export { useConsent, useConsentCategory, consentStore, openConsent, isConsented, readConsent, writeConsent, clearConsent, DEFAULT_CONSENT_CATEGORIES, CONSENT_KEY, CONSENT_ESSENTIAL, CONSENT_ANALYTICS, CONSENT_MARKETING, consentBootstrapScript, } from '@owlmeans/web-consent';
7
+ export { useConsent, useConsentCategory, consentStore, openConsent, isConsented, readConsent, writeConsent, clearConsent, DEFAULT_CONSENT_CATEGORIES, CONSENT_KEY, CONSENT_ESSENTIAL, CONSENT_FUNCTIONAL, CONSENT_ANALYTICS, CONSENT_MARKETING, CONSENT_EVENT, CONSENT_LANGUAGE_EVENT, functionalGranted, consentBootstrapScript, } from '@owlmeans/web-consent';
7
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/consent/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,eAAe,CAAA;AAC7B,OAAO,EAAE,0BAA0B,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAC3G,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAIpD,OAAO,EACL,UAAU,EAAE,kBAAkB,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EACtE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,0BAA0B,EACnE,WAAW,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,EACpE,sBAAsB,GACvB,MAAM,uBAAuB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/consent/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,eAAe,CAAA;AAC7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AACtD,OAAO,EAAE,0BAA0B,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAC3G,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAIpD,OAAO,EACL,UAAU,EAAE,kBAAkB,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EACtE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,0BAA0B,EACnE,WAAW,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,iBAAiB,EACxF,aAAa,EAAE,sBAAsB,EAAE,iBAAiB,EACxD,sBAAsB,GACvB,MAAM,uBAAuB,CAAA"}
@@ -0,0 +1,22 @@
1
+ import type { ConsentOptions } from '@owlmeans/web-consent';
2
+ /**
3
+ * Bind the application's language to the visitor's cookie choice: a language is remembered on their
4
+ * device only while they have granted `functional` storage.
5
+ *
6
+ * It does three things, and the order they take effect in is the point:
7
+ *
8
+ * 1. Installs the persistence guard in `@owlmeans/client-i18n` — so the language switcher (which
9
+ * calls `setLanguage`) stops writing `owlmeans-lng` until functional is granted, and a stored
10
+ * language counts as absent while it is not. **Call it BEFORE `prepareI18n`**: the initial
11
+ * language is resolved from storage right there.
12
+ * 2. On every consent change (`CONSENT_EVENT`) writes the choice the visitor made while storage was
13
+ * refused, if it is allowed now — a person who picked a language, then answered the dialog with
14
+ * "accept all", keeps it.
15
+ * 3. On `CONSENT_LANGUAGE_EVENT` — a language that arrived with a link and was waiting for the grant —
16
+ * switches to it now, unless the person has already picked one in this page's life: what they
17
+ * chose outranks what a link carried, and that choice is re-written over the store's own write.
18
+ *
19
+ * Returns a function that undoes all three. Safe on the server (only the guard is installed).
20
+ */
21
+ export declare const installConsentLanguage: (opts?: ConsentOptions) => (() => void);
22
+ //# sourceMappingURL=language.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language.d.ts","sourceRoot":"","sources":["../../src/consent/language.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAE3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sBAAsB,UAAW,cAAc,KAAG,CAAC,MAAM,IAAI,CA6BzE,CAAA"}
@@ -0,0 +1,49 @@
1
+ import { getExplicitLanguage, persistLanguage, setLanguage, setLanguagePersistence } from '@owlmeans/client-i18n';
2
+ import { CONSENT_EVENT, CONSENT_LANGUAGE_EVENT, functionalGranted } from '@owlmeans/web-consent';
3
+ /**
4
+ * Bind the application's language to the visitor's cookie choice: a language is remembered on their
5
+ * device only while they have granted `functional` storage.
6
+ *
7
+ * It does three things, and the order they take effect in is the point:
8
+ *
9
+ * 1. Installs the persistence guard in `@owlmeans/client-i18n` — so the language switcher (which
10
+ * calls `setLanguage`) stops writing `owlmeans-lng` until functional is granted, and a stored
11
+ * language counts as absent while it is not. **Call it BEFORE `prepareI18n`**: the initial
12
+ * language is resolved from storage right there.
13
+ * 2. On every consent change (`CONSENT_EVENT`) writes the choice the visitor made while storage was
14
+ * refused, if it is allowed now — a person who picked a language, then answered the dialog with
15
+ * "accept all", keeps it.
16
+ * 3. On `CONSENT_LANGUAGE_EVENT` — a language that arrived with a link and was waiting for the grant —
17
+ * switches to it now, unless the person has already picked one in this page's life: what they
18
+ * chose outranks what a link carried, and that choice is re-written over the store's own write.
19
+ *
20
+ * Returns a function that undoes all three. Safe on the server (only the guard is installed).
21
+ */
22
+ export const installConsentLanguage = (opts) => {
23
+ setLanguagePersistence(() => functionalGranted(opts));
24
+ if (typeof window === 'undefined') {
25
+ return () => setLanguagePersistence(null);
26
+ }
27
+ const onConsent = () => {
28
+ persistLanguage();
29
+ };
30
+ const onLanguage = (event) => {
31
+ const carried = event.detail?.language;
32
+ const chosen = getExplicitLanguage();
33
+ const language = chosen ?? carried;
34
+ if (typeof language !== 'string' || language === '') {
35
+ return;
36
+ }
37
+ setLanguage(language).catch((error) => {
38
+ console.error('[i18n] could not apply the language that was waiting for consent', error);
39
+ });
40
+ };
41
+ window.addEventListener(CONSENT_EVENT, onConsent);
42
+ window.addEventListener(CONSENT_LANGUAGE_EVENT, onLanguage);
43
+ return () => {
44
+ window.removeEventListener(CONSENT_EVENT, onConsent);
45
+ window.removeEventListener(CONSENT_LANGUAGE_EVENT, onLanguage);
46
+ setLanguagePersistence(null);
47
+ };
48
+ };
49
+ //# sourceMappingURL=language.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language.js","sourceRoot":"","sources":["../../src/consent/language.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AACjH,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAGhG;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,IAAqB,EAAgB,EAAE;IAC5E,sBAAsB,CAAC,GAAG,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAA;IAErD,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,GAAG,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAA;IAC3C,CAAC;IAED,MAAM,SAAS,GAAG,GAAS,EAAE;QAC3B,eAAe,EAAE,CAAA;IACnB,CAAC,CAAA;IACD,MAAM,UAAU,GAAG,CAAC,KAAY,EAAQ,EAAE;QACxC,MAAM,OAAO,GAAI,KAA4C,CAAC,MAAM,EAAE,QAAQ,CAAA;QAC9E,MAAM,MAAM,GAAG,mBAAmB,EAAE,CAAA;QACpC,MAAM,QAAQ,GAAG,MAAM,IAAI,OAAO,CAAA;QAClC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;YACpD,OAAM;QACR,CAAC;QACD,WAAW,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YAC7C,OAAO,CAAC,KAAK,CAAC,kEAAkE,EAAE,KAAK,CAAC,CAAA;QAC1F,CAAC,CAAC,CAAA;IACJ,CAAC,CAAA;IACD,MAAM,CAAC,gBAAgB,CAAC,aAAa,EAAE,SAAS,CAAC,CAAA;IACjD,MAAM,CAAC,gBAAgB,CAAC,sBAAsB,EAAE,UAAU,CAAC,CAAA;IAE3D,OAAO,GAAG,EAAE;QACV,MAAM,CAAC,mBAAmB,CAAC,aAAa,EAAE,SAAS,CAAC,CAAA;QACpD,MAAM,CAAC,mBAAmB,CAAC,sBAAsB,EAAE,UAAU,CAAC,CAAA;QAC9D,sBAAsB,CAAC,IAAI,CAAC,CAAA;IAC9B,CAAC,CAAA;AACH,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/web-panel",
3
- "version": "0.1.18-rc.61",
3
+ "version": "0.1.18-rc.62",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -55,16 +55,16 @@
55
55
  "@owlmeans/auth": "^0.1.18-rc.36",
56
56
  "@owlmeans/auth-common": "^0.1.18-rc.38",
57
57
  "@owlmeans/basic-envelope": "^0.1.18-rc.37",
58
- "@owlmeans/client": "^0.1.18-rc.41",
59
- "@owlmeans/client-auth": "^0.1.18-rc.48",
58
+ "@owlmeans/client": "^0.1.18-rc.42",
59
+ "@owlmeans/client-auth": "^0.1.18-rc.49",
60
60
  "@owlmeans/client-config": "^0.1.18-rc.38",
61
61
  "@owlmeans/client-context": "^0.1.18-rc.39",
62
62
  "@owlmeans/client-entrypoint": "^0.1.18-rc.38",
63
- "@owlmeans/client-flow": "^0.1.18-rc.44",
64
- "@owlmeans/client-i18n": "^0.1.18-rc.40",
65
- "@owlmeans/client-panel": "^0.1.18-rc.48",
63
+ "@owlmeans/client-flow": "^0.1.18-rc.45",
64
+ "@owlmeans/client-i18n": "^0.1.18-rc.41",
65
+ "@owlmeans/client-panel": "^0.1.18-rc.49",
66
66
  "@owlmeans/client-route": "^0.1.18-rc.39",
67
- "@owlmeans/client-socket": "^0.1.18-rc.39",
67
+ "@owlmeans/client-socket": "^0.1.18-rc.40",
68
68
  "@owlmeans/config": "^0.1.18-rc.38",
69
69
  "@owlmeans/context": "^0.1.18-rc.32",
70
70
  "@owlmeans/entrypoint": "^0.1.18-rc.37",
@@ -73,11 +73,11 @@
73
73
  "@owlmeans/i18n": "^0.1.18-rc.37",
74
74
  "@owlmeans/route": "^0.1.18-rc.33",
75
75
  "@owlmeans/state": "^0.1.18-rc.35",
76
- "@owlmeans/web-client": "^0.1.18-rc.51",
77
- "@owlmeans/web-consent": "^0.1.18-rc.32",
76
+ "@owlmeans/web-client": "^0.1.18-rc.52",
77
+ "@owlmeans/web-consent": "^0.1.18-rc.33",
78
78
  "@owlmeans/web-db": "^0.1.18-rc.39",
79
- "@owlmeans/web-flow": "^0.1.18-rc.43",
80
- "@owlmeans/web-router": "^0.1.18-rc.39",
79
+ "@owlmeans/web-flow": "^0.1.18-rc.44",
80
+ "@owlmeans/web-router": "^0.1.18-rc.40",
81
81
  "i18next-browser-languagedetector": "^8.0.0",
82
82
  "qrcode.react": "^4.2.0",
83
83
  "react-google-recaptcha": "^3.1.0",
@@ -103,7 +103,7 @@
103
103
  },
104
104
  "devDependencies": {
105
105
  "@owlmeans/dep-config": "workspace:*",
106
- "@owlmeans/test-ui": "^0.1.18-rc.39",
106
+ "@owlmeans/test-ui": "^0.1.18-rc.40",
107
107
  "@radix-ui/react-alert-dialog": "^1.1.15",
108
108
  "@radix-ui/react-dialog": "^1.1.15",
109
109
  "@radix-ui/react-label": "^2.1.0",
@@ -1,6 +1,7 @@
1
1
  export * from './component.js'
2
2
  export * from './menu-widget.js'
3
3
  export * from './presence.js'
4
+ export { installConsentLanguage } from './language.js'
4
5
  export { createConsentWidgetService, appendConsentWidgetService, CONSENT_WIDGET_STATE } from './service.js'
5
6
  export { CONSENT_WIDGET_SERVICE } from './consts.js'
6
7
  export type {
@@ -9,7 +10,8 @@ export type {
9
10
  export {
10
11
  useConsent, useConsentCategory, consentStore, openConsent, isConsented,
11
12
  readConsent, writeConsent, clearConsent, DEFAULT_CONSENT_CATEGORIES,
12
- CONSENT_KEY, CONSENT_ESSENTIAL, CONSENT_ANALYTICS, CONSENT_MARKETING,
13
+ CONSENT_KEY, CONSENT_ESSENTIAL, CONSENT_FUNCTIONAL, CONSENT_ANALYTICS, CONSENT_MARKETING,
14
+ CONSENT_EVENT, CONSENT_LANGUAGE_EVENT, functionalGranted,
13
15
  consentBootstrapScript,
14
16
  } from '@owlmeans/web-consent'
15
17
  export type {
@@ -0,0 +1,53 @@
1
+ import { getExplicitLanguage, persistLanguage, setLanguage, setLanguagePersistence } from '@owlmeans/client-i18n'
2
+ import { CONSENT_EVENT, CONSENT_LANGUAGE_EVENT, functionalGranted } from '@owlmeans/web-consent'
3
+ import type { ConsentOptions } from '@owlmeans/web-consent'
4
+
5
+ /**
6
+ * Bind the application's language to the visitor's cookie choice: a language is remembered on their
7
+ * device only while they have granted `functional` storage.
8
+ *
9
+ * It does three things, and the order they take effect in is the point:
10
+ *
11
+ * 1. Installs the persistence guard in `@owlmeans/client-i18n` — so the language switcher (which
12
+ * calls `setLanguage`) stops writing `owlmeans-lng` until functional is granted, and a stored
13
+ * language counts as absent while it is not. **Call it BEFORE `prepareI18n`**: the initial
14
+ * language is resolved from storage right there.
15
+ * 2. On every consent change (`CONSENT_EVENT`) writes the choice the visitor made while storage was
16
+ * refused, if it is allowed now — a person who picked a language, then answered the dialog with
17
+ * "accept all", keeps it.
18
+ * 3. On `CONSENT_LANGUAGE_EVENT` — a language that arrived with a link and was waiting for the grant —
19
+ * switches to it now, unless the person has already picked one in this page's life: what they
20
+ * chose outranks what a link carried, and that choice is re-written over the store's own write.
21
+ *
22
+ * Returns a function that undoes all three. Safe on the server (only the guard is installed).
23
+ */
24
+ export const installConsentLanguage = (opts?: ConsentOptions): (() => void) => {
25
+ setLanguagePersistence(() => functionalGranted(opts))
26
+
27
+ if (typeof window === 'undefined') {
28
+ return () => setLanguagePersistence(null)
29
+ }
30
+
31
+ const onConsent = (): void => {
32
+ persistLanguage()
33
+ }
34
+ const onLanguage = (event: Event): void => {
35
+ const carried = (event as CustomEvent<{ language?: string }>).detail?.language
36
+ const chosen = getExplicitLanguage()
37
+ const language = chosen ?? carried
38
+ if (typeof language !== 'string' || language === '') {
39
+ return
40
+ }
41
+ setLanguage(language).catch((error: unknown) => {
42
+ console.error('[i18n] could not apply the language that was waiting for consent', error)
43
+ })
44
+ }
45
+ window.addEventListener(CONSENT_EVENT, onConsent)
46
+ window.addEventListener(CONSENT_LANGUAGE_EVENT, onLanguage)
47
+
48
+ return () => {
49
+ window.removeEventListener(CONSENT_EVENT, onConsent)
50
+ window.removeEventListener(CONSENT_LANGUAGE_EVENT, onLanguage)
51
+ setLanguagePersistence(null)
52
+ }
53
+ }
@@ -0,0 +1,82 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import { persistLanguage, setLanguage, setLanguagePersistence } from '@owlmeans/client-i18n'
3
+ import { CONSENT_EVENT, CONSENT_KEY, CONSENT_LANGUAGE_EVENT } from '@owlmeans/web-consent'
4
+ import { installConsentLanguage } from '../src/consent/language.js'
5
+
6
+ /** A window that is just an event bus, and a localStorage that is a map: enough for the binding. */
7
+ const env = () => {
8
+ const store = new Map<string, string>()
9
+ const listeners = new Map<string, Set<(e: any) => void>>()
10
+ ;(globalThis as any).localStorage = {
11
+ getItem: (k: string) => store.get(k) ?? null,
12
+ setItem: (k: string, v: string) => { store.set(k, v) },
13
+ removeItem: (k: string) => { store.delete(k) },
14
+ }
15
+ ;(globalThis as any).window = {
16
+ addEventListener: (t: string, f: any) => { (listeners.get(t) ?? listeners.set(t, new Set()).get(t)!).add(f) },
17
+ removeEventListener: (t: string, f: any) => { listeners.get(t)?.delete(f) },
18
+ dispatchEvent: (e: any) => { listeners.get(e.type)?.forEach(f => f(e)) },
19
+ }
20
+ ;(globalThis as any).CustomEvent = class { constructor(public type: string, public init: any) {} get detail() { return this.init?.detail } }
21
+
22
+ return { store, listeners, emit: (type: string, detail?: unknown) => (globalThis as any).window.dispatchEvent(new (globalThis as any).CustomEvent(type, { detail })) }
23
+ }
24
+
25
+ describe('installConsentLanguage', () => {
26
+ let e: ReturnType<typeof env>
27
+ let off: () => void
28
+ const record = (functional: boolean) => e.store.set(CONSENT_KEY, JSON.stringify({ v: 2, essential: true, functional, analytics: false, marketing: false }))
29
+
30
+ beforeEach(() => { e = env(); off = installConsentLanguage() })
31
+ afterEach(() => { off(); setLanguagePersistence(null); delete (globalThis as any).window })
32
+
33
+ test('the switcher\'s choice is not remembered without a functional grant', async () => {
34
+ await setLanguage('pl')
35
+
36
+ expect(e.store.has('owlmeans-lng')).toBe(false)
37
+ })
38
+
39
+ test('…and is written when the visitor grants functional, on the consent event', async () => {
40
+ await setLanguage('pl')
41
+ record(true)
42
+ e.emit(CONSENT_EVENT, { record: {} })
43
+
44
+ expect(e.store.get('owlmeans-lng')).toBe('pl')
45
+ })
46
+
47
+ test('a refusal writes nothing on the consent event', async () => {
48
+ await setLanguage('pl')
49
+ record(false)
50
+ e.emit(CONSENT_EVENT, { record: {} })
51
+
52
+ expect(e.store.has('owlmeans-lng')).toBe(false)
53
+ })
54
+
55
+ test('a language that was waiting for the grant is applied and stored when it arrives', async () => {
56
+ record(true)
57
+ e.emit(CONSENT_LANGUAGE_EVENT, { language: 'de' })
58
+ await new Promise(resolve => setTimeout(resolve, 0))
59
+
60
+ expect(e.store.get('owlmeans-lng')).toBe('de')
61
+ })
62
+
63
+ test('what the person picked in this page life outranks the language the link carried', async () => {
64
+ await setLanguage('fr')
65
+ record(true)
66
+ e.emit(CONSENT_EVENT, { record: {} })
67
+ e.emit(CONSENT_LANGUAGE_EVENT, { language: 'de' })
68
+ await new Promise(resolve => setTimeout(resolve, 0))
69
+
70
+ expect(e.store.get('owlmeans-lng')).toBe('fr')
71
+ })
72
+
73
+ test('the returned function removes the listeners and the guard', async () => {
74
+ off()
75
+ expect(e.listeners.get(CONSENT_EVENT)?.size ?? 0).toBe(0)
76
+ expect(e.listeners.get(CONSENT_LANGUAGE_EVENT)?.size ?? 0).toBe(0)
77
+
78
+ await setLanguage('es')
79
+ expect(e.store.get('owlmeans-lng')).toBe('es')
80
+ expect(persistLanguage()).toBe(false)
81
+ })
82
+ })