@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 +1 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-panel/SKILL.md +2 -1
- package/build/consent/index.d.ts +2 -1
- package/build/consent/index.d.ts.map +1 -1
- package/build/consent/index.js +2 -1
- package/build/consent/index.js.map +1 -1
- package/build/consent/language.d.ts +22 -0
- package/build/consent/language.d.ts.map +1 -0
- package/build/consent/language.js +49 -0
- package/build/consent/language.js.map +1 -0
- package/package.json +12 -12
- package/src/consent/index.ts +3 -1
- package/src/consent/language.ts +53 -0
- package/tests/consent-language.spec.ts +82 -0
package/README.md
CHANGED
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/web-panel",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
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.
|
|
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` |
|
package/build/consent/index.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/build/consent/index.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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.
|
|
59
|
-
"@owlmeans/client-auth": "^0.1.18-rc.
|
|
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.
|
|
64
|
-
"@owlmeans/client-i18n": "^0.1.18-rc.
|
|
65
|
-
"@owlmeans/client-panel": "^0.1.18-rc.
|
|
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.
|
|
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.
|
|
77
|
-
"@owlmeans/web-consent": "^0.1.18-rc.
|
|
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.
|
|
80
|
-
"@owlmeans/web-router": "^0.1.18-rc.
|
|
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.
|
|
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",
|
package/src/consent/index.ts
CHANGED
|
@@ -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
|
+
})
|