Add i18n support to cookie banner JS client
Replace all hardcoded user-facing strings with server-driven
texts from config.texts. Add language detection chain
(data-lang > html lang > navigator.language), pass ?lang
query param on config fetch, and use {{variable}} interpolation
for dynamic content in templates.
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import type { BannerTexts } from "./i18n";
|
||||
import { interpolate } from "./i18n";
|
||||
import { removeCookies } from "./cookie-utils";
|
||||
import { LOCK_ICON } from "./html";
|
||||
|
||||
@@ -111,6 +113,7 @@ function createPlaceholder(
|
||||
el: Element,
|
||||
category: string,
|
||||
label?: string,
|
||||
texts?: BannerTexts,
|
||||
): void {
|
||||
if (el.hasAttribute(ATTR_HIDDEN)) return;
|
||||
|
||||
@@ -152,10 +155,15 @@ function createPlaceholder(
|
||||
}
|
||||
}
|
||||
|
||||
const phText = texts?.placeholder_text
|
||||
? interpolate(texts.placeholder_text, { category: escapeHtml(displayLabel) })
|
||||
: `This content requires <strong>${escapeHtml(displayLabel)}</strong> cookies.`;
|
||||
const phButton = texts?.placeholder_button ?? "Manage cookie preferences";
|
||||
|
||||
placeholder.innerHTML = [
|
||||
`<span class="probo-ph-icon">${LOCK_ICON}</span>`,
|
||||
`<p class="probo-ph-text">This content requires <strong>${escapeHtml(displayLabel)}</strong> cookies.</p>`,
|
||||
`<button type="button" class="probo-ph-link">Manage cookie preferences</button>`,
|
||||
`<p class="probo-ph-text">${phText}</p>`,
|
||||
`<button type="button" class="probo-ph-link">${escapeHtml(phButton)}</button>`,
|
||||
].join("");
|
||||
|
||||
placeholder.querySelector(".probo-ph-link")!.addEventListener("click", () => {
|
||||
@@ -362,13 +370,14 @@ export function activateElements(
|
||||
export function addPlaceholders(
|
||||
consentData: Record<string, boolean>,
|
||||
categoryLabels: Record<string, string>,
|
||||
texts?: BannerTexts,
|
||||
): void {
|
||||
const elements = document.querySelectorAll(`[${ATTR_CATEGORY}]`);
|
||||
for (const el of elements) {
|
||||
const category = el.getAttribute(ATTR_CATEGORY);
|
||||
if (!category || consentData[category]) continue;
|
||||
if (!VISUAL_TAGS.has(el.tagName)) continue;
|
||||
createPlaceholder(el, category, categoryLabels[category]);
|
||||
createPlaceholder(el, category, categoryLabels[category], texts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,16 +385,18 @@ function tryPlaceholder(
|
||||
el: Element,
|
||||
consentData: Record<string, boolean>,
|
||||
categoryLabels: Record<string, string>,
|
||||
texts?: BannerTexts,
|
||||
): void {
|
||||
const category = el.getAttribute(ATTR_CATEGORY);
|
||||
if (!category || consentData[category]) return;
|
||||
if (!VISUAL_TAGS.has(el.tagName)) return;
|
||||
createPlaceholder(el, category, categoryLabels[category]);
|
||||
createPlaceholder(el, category, categoryLabels[category], texts);
|
||||
}
|
||||
|
||||
export function observeAndActivate(
|
||||
consentData: Record<string, boolean>,
|
||||
categoryLabels: Record<string, string>,
|
||||
texts?: BannerTexts,
|
||||
): MutationObserver {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
@@ -396,13 +407,13 @@ export function observeAndActivate(
|
||||
|
||||
if (node.hasAttribute(ATTR_CATEGORY)) {
|
||||
tryActivate(node, consentData);
|
||||
tryPlaceholder(node, consentData, categoryLabels);
|
||||
tryPlaceholder(node, consentData, categoryLabels, texts);
|
||||
}
|
||||
|
||||
const nested = node.querySelectorAll(`[${ATTR_CATEGORY}]`);
|
||||
for (const el of nested) {
|
||||
tryActivate(el, consentData);
|
||||
tryPlaceholder(el, consentData, categoryLabels);
|
||||
tryPlaceholder(el, consentData, categoryLabels, texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,14 @@ import { COOKIE_NAME, getConsentCookie, setConsentCookie } from "./cookie";
|
||||
import { CookieDetector } from "./detector";
|
||||
import { NotFoundError } from "./errors";
|
||||
import { fetchJSON } from "./http";
|
||||
import type { BannerTexts } from "./i18n";
|
||||
import { detectLanguage } from "./i18n";
|
||||
import { enqueue, flush } from "./queue";
|
||||
import { getOrCreateVisitorId } from "./visitor";
|
||||
|
||||
export type { BannerTexts } from "./i18n";
|
||||
export { interpolate } from "./i18n";
|
||||
|
||||
export interface CookieItem {
|
||||
name: string;
|
||||
duration: string;
|
||||
@@ -41,11 +46,15 @@ export interface Category {
|
||||
export interface BannerConfig {
|
||||
banner_id: string;
|
||||
version: number;
|
||||
language: string;
|
||||
default_language: string;
|
||||
available_languages: string[];
|
||||
privacy_policy_url: string;
|
||||
consent_expiry_days: number;
|
||||
consent_mode: "OPT_IN" | "OPT_OUT";
|
||||
show_branding: boolean;
|
||||
categories: Category[];
|
||||
texts: BannerTexts;
|
||||
}
|
||||
|
||||
export type ConsentAction = "ACCEPT_ALL" | "REJECT_ALL" | "CUSTOMIZE" | "GPC";
|
||||
@@ -68,12 +77,14 @@ export interface ConsentRecord {
|
||||
export interface CookieBannerClientOptions {
|
||||
bannerId: string;
|
||||
baseUrl: string;
|
||||
lang?: string;
|
||||
}
|
||||
|
||||
export class CookieBannerClient {
|
||||
private readonly baseUrl: URL;
|
||||
private readonly bannerId: string;
|
||||
private readonly visitorId: string;
|
||||
private readonly lang: string;
|
||||
|
||||
private bannerConfig: BannerConfig | null = null;
|
||||
private consent: VisitorConsent | null = null;
|
||||
@@ -88,10 +99,14 @@ export class CookieBannerClient {
|
||||
this.baseUrl = new URL(base);
|
||||
this.bannerId = config.bannerId;
|
||||
this.visitorId = getOrCreateVisitorId(config.bannerId);
|
||||
this.lang = detectLanguage(config.lang);
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
const configUrl = new URL(`${this.bannerId}/config`, this.baseUrl);
|
||||
if (this.lang) {
|
||||
configUrl.searchParams.set("lang", this.lang);
|
||||
}
|
||||
const config = await fetchJSON<BannerConfig>(configUrl);
|
||||
this.bannerConfig = config;
|
||||
|
||||
@@ -237,13 +252,14 @@ export class CookieBannerClient {
|
||||
categoryLabels[cat.name] = cat.name;
|
||||
}
|
||||
|
||||
const texts = this.config.texts;
|
||||
deactivateElements(consentData, categoryCookies, categoryLabels);
|
||||
activateElements(consentData);
|
||||
addPlaceholders(consentData, categoryLabels);
|
||||
addPlaceholders(consentData, categoryLabels, texts);
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
this.observer = observeAndActivate(consentData, categoryLabels);
|
||||
this.observer = observeAndActivate(consentData, categoryLabels, texts);
|
||||
}
|
||||
|
||||
private startDetector(config: BannerConfig): void {
|
||||
|
||||
@@ -24,7 +24,7 @@ export class ProboCookieBannerRoot extends ProboElement implements ProboRootElem
|
||||
private _draft: ConsentDraft = {};
|
||||
|
||||
static get observedAttributes(): string[] {
|
||||
return ["banner-id", "base-url", "reopen-widget"];
|
||||
return ["banner-id", "base-url", "reopen-widget", "lang"];
|
||||
}
|
||||
|
||||
get client(): CookieBannerClient {
|
||||
@@ -129,7 +129,9 @@ export class ProboCookieBannerRoot extends ProboElement implements ProboRootElem
|
||||
return;
|
||||
}
|
||||
|
||||
this._client = new CookieBannerClient({ bannerId, baseUrl });
|
||||
const lang = this.getAttribute("lang") ?? undefined;
|
||||
|
||||
this._client = new CookieBannerClient({ bannerId, baseUrl, lang });
|
||||
|
||||
try {
|
||||
await this._client.load();
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import type { CookieItem } from "../client";
|
||||
import { type BannerTexts, interpolate } from "../i18n";
|
||||
import { ProboElement } from "./base";
|
||||
import type { ProboCategory } from "./category";
|
||||
import type { ProboCookieBannerRoot } from "./cookie-banner-root";
|
||||
|
||||
export class ProboCookieList extends ProboElement {
|
||||
private category: ProboCategory | null = null;
|
||||
@@ -35,13 +37,16 @@ export class ProboCookieList extends ProboElement {
|
||||
private stamp(): void {
|
||||
if (!this.template || !this.category) return;
|
||||
|
||||
const root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
||||
const texts = root?.bannerConfig?.texts;
|
||||
|
||||
const cookies = this.category.cookies;
|
||||
for (const cookie of cookies) {
|
||||
this.stampCookie(cookie);
|
||||
this.stampCookie(cookie, texts);
|
||||
}
|
||||
}
|
||||
|
||||
private stampCookie(cookie: CookieItem): void {
|
||||
private stampCookie(cookie: CookieItem, texts?: BannerTexts): void {
|
||||
if (!this.template) return;
|
||||
|
||||
const wrapper = document.createElement("probo-cookie");
|
||||
@@ -52,6 +57,10 @@ export class ProboCookieList extends ProboElement {
|
||||
duration: cookie.duration,
|
||||
description: cookie.description,
|
||||
});
|
||||
this.fillLabels(clone, texts, {
|
||||
description: cookie.description,
|
||||
duration: cookie.duration,
|
||||
});
|
||||
|
||||
wrapper.appendChild(clone);
|
||||
this.appendChild(wrapper);
|
||||
@@ -68,6 +77,25 @@ export class ProboCookieList extends ProboElement {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fillLabels(
|
||||
fragment: DocumentFragment,
|
||||
texts: BannerTexts | undefined,
|
||||
values: Record<string, string>,
|
||||
): void {
|
||||
const labels = fragment.querySelectorAll("[data-label]");
|
||||
for (const el of labels) {
|
||||
const key = el.getAttribute("data-label")!;
|
||||
const slotName = key.replace("label_", "");
|
||||
const value = values[slotName] ?? "";
|
||||
const tpl = texts?.[key];
|
||||
if (tpl) {
|
||||
el.textContent = interpolate(tpl, { value });
|
||||
const slot = el.querySelector(`[data-slot="${slotName}"]`);
|
||||
if (slot) slot.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ProboCookie extends ProboElement {}
|
||||
|
||||
@@ -26,7 +26,7 @@ export class ProboSettingsButton extends HTMLElement {
|
||||
}
|
||||
|
||||
static get observedAttributes(): string[] {
|
||||
return ["position"];
|
||||
return ["position", "aria-settings-label"];
|
||||
}
|
||||
|
||||
private get position(): string {
|
||||
@@ -89,6 +89,11 @@ export class ProboSettingsButton extends HTMLElement {
|
||||
|
||||
const btn = this.shadow.querySelector("button");
|
||||
btn?.addEventListener("click", this.handleClick);
|
||||
|
||||
const ariaLabel = this.getAttribute("aria-settings-label");
|
||||
if (ariaLabel && btn) {
|
||||
btn.setAttribute("aria-label", ariaLabel);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
|
||||
@@ -12,25 +12,28 @@
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
// [unitSeconds, singular, plural, snapBuffer]
|
||||
import type { BannerTexts } from "./i18n";
|
||||
import { interpolate } from "./i18n";
|
||||
|
||||
// [unitSeconds, textKey, snapBuffer]
|
||||
// snapBuffer: if the remainder is within this many seconds of the next
|
||||
// whole unit, round up instead of carrying into smaller units.
|
||||
const DURATION_UNITS: [number, string, string, number][] = [
|
||||
[365 * 24 * 3600, "year", "years", 21 * 24 * 3600],
|
||||
[30 * 24 * 3600, "month", "months", 2 * 24 * 3600],
|
||||
[7 * 24 * 3600, "week", "weeks", 12 * 3600],
|
||||
[24 * 3600, "day", "days", 0],
|
||||
[3600, "hour", "hours", 5 * 60],
|
||||
[60, "minute", "minutes", 15],
|
||||
const DURATION_UNITS: [number, string, number][] = [
|
||||
[365 * 24 * 3600, "duration_year", 21 * 24 * 3600],
|
||||
[30 * 24 * 3600, "duration_month", 2 * 24 * 3600],
|
||||
[7 * 24 * 3600, "duration_week", 12 * 3600],
|
||||
[24 * 3600, "duration_day", 0],
|
||||
[3600, "duration_hour", 5 * 60],
|
||||
[60, "duration_minute", 15],
|
||||
];
|
||||
|
||||
function humanizeDuration(seconds: number): string {
|
||||
if (seconds <= 0) return "session";
|
||||
function humanizeDuration(seconds: number, texts?: BannerTexts): string {
|
||||
if (seconds <= 0) return texts?.duration_session ?? "session";
|
||||
|
||||
let remaining = seconds;
|
||||
const parts: string[] = [];
|
||||
|
||||
for (const [unit, singular, plural, snap] of DURATION_UNITS) {
|
||||
for (const [unit, key, snap] of DURATION_UNITS) {
|
||||
if (remaining >= unit) {
|
||||
let count = Math.floor(remaining / unit);
|
||||
const leftover = remaining - count * unit;
|
||||
@@ -44,11 +47,13 @@ function humanizeDuration(seconds: number): string {
|
||||
remaining = leftover;
|
||||
}
|
||||
|
||||
parts.push(count === 1 ? `1 ${singular}` : `${count} ${plural}`);
|
||||
const tplKey = count === 1 ? `${key}_one` : `${key}_other`;
|
||||
const tpl = texts?.[tplKey] ?? (count === 1 ? `{{count}} ${key.replace("duration_", "")}` : `{{count}} ${key.replace("duration_", "")}s`);
|
||||
parts.push(interpolate(tpl, { count: String(count) }));
|
||||
}
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts.join(", ") : "session";
|
||||
return parts.length > 0 ? parts.join(", ") : texts?.duration_session ?? "session";
|
||||
}
|
||||
|
||||
export function parseCookieName(raw: string): string {
|
||||
@@ -57,15 +62,16 @@ export function parseCookieName(raw: string): string {
|
||||
return raw.substring(0, eqIdx).trim();
|
||||
}
|
||||
|
||||
export function parseDuration(raw: string): string {
|
||||
export function parseDuration(raw: string, texts?: BannerTexts): string {
|
||||
const session = texts?.duration_session ?? "session";
|
||||
const parts = raw.split(";").map((s) => s.trim());
|
||||
|
||||
for (const part of parts) {
|
||||
const lower = part.toLowerCase();
|
||||
if (lower.startsWith("max-age=")) {
|
||||
const val = parseInt(part.substring(8), 10);
|
||||
if (isNaN(val) || val <= 0) return "session";
|
||||
return humanizeDuration(val);
|
||||
if (isNaN(val) || val <= 0) return session;
|
||||
return humanizeDuration(val, texts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,16 +80,16 @@ export function parseDuration(raw: string): string {
|
||||
if (lower.startsWith("expires=")) {
|
||||
const dateStr = part.substring(8);
|
||||
const expires = new Date(dateStr);
|
||||
if (isNaN(expires.getTime())) return "session";
|
||||
if (isNaN(expires.getTime())) return session;
|
||||
const deltaSeconds = Math.round(
|
||||
(expires.getTime() - Date.now()) / 1000,
|
||||
);
|
||||
if (deltaSeconds <= 0) return "session";
|
||||
return humanizeDuration(deltaSeconds);
|
||||
if (deltaSeconds <= 0) return session;
|
||||
return humanizeDuration(deltaSeconds, texts);
|
||||
}
|
||||
}
|
||||
|
||||
return "session";
|
||||
return session;
|
||||
}
|
||||
|
||||
export function isDeletion(raw: string): boolean {
|
||||
|
||||
37
packages/cookie-banner/src/i18n.ts
Normal file
37
packages/cookie-banner/src/i18n.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
export interface BannerTexts {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export function detectLanguage(explicit?: string): string {
|
||||
if (explicit) return explicit;
|
||||
|
||||
const htmlLang = document.documentElement.lang;
|
||||
if (htmlLang) return htmlLang.split("-")[0];
|
||||
|
||||
if (typeof navigator !== "undefined" && navigator.language) {
|
||||
return navigator.language.split("-")[0];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
export function interpolate(
|
||||
template: string,
|
||||
vars: Record<string, string>,
|
||||
): string {
|
||||
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
||||
}
|
||||
@@ -38,6 +38,11 @@ if (script) {
|
||||
el.setAttribute("reopen-widget", reopenWidget);
|
||||
}
|
||||
|
||||
const lang = script.getAttribute("data-lang");
|
||||
if (lang) {
|
||||
el.setAttribute("lang", lang);
|
||||
}
|
||||
|
||||
document.body.appendChild(el);
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { registerComponents } from "../components";
|
||||
import type { ProboCookieBannerRoot } from "../components/cookie-banner-root";
|
||||
import type { BannerConfig } from "../client";
|
||||
import { interpolate } from "../i18n";
|
||||
import { BRANDING, CHEVRON_DOWN, CLOSE_ICON } from "../html";
|
||||
import { THEMED_STYLES } from "./styles";
|
||||
|
||||
@@ -27,7 +28,7 @@ export class ProboThemedBanner extends HTMLElement {
|
||||
}
|
||||
|
||||
static get observedAttributes(): string[] {
|
||||
return ["banner-id", "base-url", "reopen-widget"];
|
||||
return ["banner-id", "base-url", "reopen-widget", "lang"];
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
@@ -44,21 +45,21 @@ export class ProboThemedBanner extends HTMLElement {
|
||||
const position = this.getAttribute("position") ?? "bottom-left";
|
||||
const reopenWidget = this.getAttribute("reopen-widget");
|
||||
const reopenAttr = reopenWidget ? ` reopen-widget="${this.esc(reopenWidget)}"` : "";
|
||||
const lang = this.getAttribute("lang");
|
||||
const langAttr = lang ? ` lang="${this.esc(lang)}"` : "";
|
||||
|
||||
this.shadow.innerHTML = `
|
||||
<style>${THEMED_STYLES}</style>
|
||||
<probo-cookie-banner-root banner-id="${this.esc(bannerId)}" base-url="${this.esc(baseUrl)}"${reopenAttr}>
|
||||
<probo-cookie-banner-root banner-id="${this.esc(bannerId)}" base-url="${this.esc(baseUrl)}"${reopenAttr}${langAttr}>
|
||||
<probo-banner>
|
||||
<div class="floating" data-position="${this.esc(position)}">
|
||||
<div class="card" role="dialog" aria-modal="true" aria-labelledby="probo-banner-title" aria-describedby="probo-banner-desc">
|
||||
<p class="title" id="probo-banner-title">Cookie Preferences</p>
|
||||
<p class="description" id="probo-banner-desc" data-description>
|
||||
We use cookies to improve your experience and analyze site traffic.
|
||||
</p>
|
||||
<p class="title" id="probo-banner-title" data-text="banner_title"></p>
|
||||
<p class="description" id="probo-banner-desc" data-text="banner_description"></p>
|
||||
<div class="buttons">
|
||||
<probo-accept-button><button class="btn btn-primary">Accept all</button></probo-accept-button>
|
||||
<probo-reject-button><button class="btn">Reject all</button></probo-reject-button>
|
||||
<probo-customize-button><button class="btn btn-link">Customize</button></probo-customize-button>
|
||||
<probo-accept-button><button class="btn btn-primary" data-text="button_accept_all"></button></probo-accept-button>
|
||||
<probo-reject-button><button class="btn" data-text="button_reject_all"></button></probo-reject-button>
|
||||
<probo-customize-button><button class="btn btn-link" data-text="button_customize"></button></probo-customize-button>
|
||||
</div>
|
||||
${BRANDING}
|
||||
</div>
|
||||
@@ -70,16 +71,16 @@ export class ProboThemedBanner extends HTMLElement {
|
||||
<div class="card" role="dialog" aria-modal="true" aria-labelledby="probo-panel-title" aria-describedby="probo-panel-desc">
|
||||
<div class="panel-header">
|
||||
<div class="panel-header-title">
|
||||
<p class="title" id="probo-panel-title" style="margin:0">Customise Preferences</p>
|
||||
<button class="panel-close" data-action="back" aria-label="Close">
|
||||
<p class="title" id="probo-panel-title" style="margin:0" data-text="panel_title"></p>
|
||||
<button class="panel-close" data-action="back" data-aria-text="aria_close">
|
||||
${CLOSE_ICON}
|
||||
</button>
|
||||
</div>
|
||||
<p class="description" id="probo-panel-desc">Choose which cookie categories to allow. Necessary cookies are always active as they are needed for the site to work.</p>
|
||||
<p class="description" id="probo-panel-desc" data-text="panel_description"></p>
|
||||
</div>
|
||||
<probo-category-list>
|
||||
<template>
|
||||
<button class="cookie-toggle" data-action="toggle-cookies" aria-expanded="false" aria-label="Show cookie details">
|
||||
<button class="cookie-toggle" data-action="toggle-cookies" aria-expanded="false" data-aria-text="aria_show_details">
|
||||
${CHEVRON_DOWN}
|
||||
</button>
|
||||
<div class="category-header">
|
||||
@@ -98,8 +99,8 @@ export class ProboThemedBanner extends HTMLElement {
|
||||
<template>
|
||||
<div class="cookie-item">
|
||||
<span class="cookie-name" data-slot="name"></span>
|
||||
<span class="cookie-detail"><span class="cookie-label">Description:</span> <span data-slot="description"></span></span>
|
||||
<span class="cookie-detail"><span class="cookie-label">Duration:</span> <span data-slot="duration"></span></span>
|
||||
<span class="cookie-detail" data-label="label_description"><span data-slot="description"></span></span>
|
||||
<span class="cookie-detail" data-label="label_duration"><span data-slot="duration"></span></span>
|
||||
</div>
|
||||
</template>
|
||||
</probo-cookie-list>
|
||||
@@ -107,10 +108,10 @@ export class ProboThemedBanner extends HTMLElement {
|
||||
</probo-category-list>
|
||||
<div class="footer">
|
||||
<div class="buttons">
|
||||
<probo-accept-button><button class="btn btn-primary">Accept all</button></probo-accept-button>
|
||||
<probo-reject-button><button class="btn">Reject all</button></probo-reject-button>
|
||||
<probo-accept-button><button class="btn btn-primary" data-text="button_accept_all"></button></probo-accept-button>
|
||||
<probo-reject-button><button class="btn" data-text="button_reject_all"></button></probo-reject-button>
|
||||
<probo-save-button>
|
||||
<button class="btn btn-link" style="flex:1">Save preferences</button>
|
||||
<button class="btn btn-link" style="flex:1" data-text="button_save"></button>
|
||||
</probo-save-button>
|
||||
</div>
|
||||
${BRANDING}
|
||||
@@ -127,7 +128,7 @@ export class ProboThemedBanner extends HTMLElement {
|
||||
|
||||
root.addEventListener("probo-ready", (e: Event) => {
|
||||
const config = (e as CustomEvent).detail.config as BannerConfig;
|
||||
this.updateDescription(config);
|
||||
this.applyTexts(config);
|
||||
if (!config.show_branding) {
|
||||
this.shadow.querySelectorAll("[data-branding]").forEach(el => {
|
||||
(el as HTMLElement).setAttribute("hidden", "");
|
||||
@@ -154,19 +155,51 @@ export class ProboThemedBanner extends HTMLElement {
|
||||
btn.classList.remove("open");
|
||||
}
|
||||
btn.setAttribute("aria-expanded", String(open));
|
||||
btn.setAttribute("aria-label", open ? "Hide cookie details" : "Show cookie details");
|
||||
const texts = root.bannerConfig?.texts;
|
||||
const showLabel = texts?.aria_show_details ?? "Show cookie details";
|
||||
const hideLabel = texts?.aria_hide_details ?? "Hide cookie details";
|
||||
btn.setAttribute("aria-label", open ? hideLabel : showLabel);
|
||||
});
|
||||
}
|
||||
|
||||
private updateDescription(config: BannerConfig): void {
|
||||
const el = this.shadow.querySelector("[data-description]");
|
||||
if (!el) return;
|
||||
private applyTexts(config: BannerConfig): void {
|
||||
const texts = config.texts ?? {};
|
||||
|
||||
let html = "We use cookies to improve your experience and analyze site traffic.";
|
||||
if (config.privacy_policy_url) {
|
||||
html += ` <a href="${this.esc(config.privacy_policy_url)}" target="_blank" rel="noopener noreferrer">Privacy Policy</a>`;
|
||||
const necessaryCategory = config.categories.find(c => c.kind === "NECESSARY");
|
||||
const necessaryCategoryName = necessaryCategory?.name ?? "Necessary";
|
||||
|
||||
this.shadow.querySelectorAll("[data-text]").forEach(el => {
|
||||
const key = el.getAttribute("data-text")!;
|
||||
const raw = texts[key];
|
||||
if (!raw) return;
|
||||
|
||||
if (key === "banner_description") {
|
||||
let link = "";
|
||||
if (config.privacy_policy_url) {
|
||||
const linkText = this.esc(texts.privacy_policy_link_text ?? "Privacy Policy");
|
||||
link = `<a href="${this.esc(config.privacy_policy_url)}" target="_blank" rel="noopener noreferrer">${linkText}</a>`;
|
||||
}
|
||||
el.innerHTML = interpolate(raw, { privacy_policy_link: link });
|
||||
} else if (key === "panel_description") {
|
||||
el.textContent = interpolate(raw, { necessary_category: necessaryCategoryName });
|
||||
} else {
|
||||
el.textContent = raw;
|
||||
}
|
||||
});
|
||||
|
||||
this.shadow.querySelectorAll("[data-aria-text]").forEach(el => {
|
||||
const key = el.getAttribute("data-aria-text")!;
|
||||
const raw = texts[key];
|
||||
if (raw) el.setAttribute("aria-label", raw);
|
||||
});
|
||||
|
||||
const settingsBtn = this.shadow.querySelector("probo-settings-button");
|
||||
if (settingsBtn) {
|
||||
const ariaText = texts.aria_cookie_settings;
|
||||
if (ariaText) {
|
||||
settingsBtn.setAttribute("aria-settings-label", ariaText);
|
||||
}
|
||||
}
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
private esc(str: string): string {
|
||||
|
||||
Reference in New Issue
Block a user