@ecomconsult/consentkit 0.5.13 → 0.5.15

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
@@ -29,7 +29,7 @@ Vanilla ES2020, zero dependencies, no build step.
29
29
  - **Equal-weight buttons, no pre-ticked boxes** — the consent invariants are
30
30
  fixed by design, see [CONTRIBUTING.md](https://github.com/vermoh/ConsentKit/blob/main/CONTRIBUTING.md)
31
31
 
32
- > **Status: prototype (v0.5.13).** The core, the UI and the demo are verified in
32
+ > **Status: prototype (v0.5.15).** The core, the UI and the demo are verified in
33
33
  > a browser and covered by an automated suite (`npm test`); several distribution
34
34
  > paths are not yet tested against live systems. See
35
35
  > [Project status](#project-status) before shipping this to production.
@@ -193,6 +193,8 @@ Pass any subset to `init()`. Nested objects merge with the defaults.
193
193
  | `texts.policyUrl` | `string` | — | v0.5.0. Cookie policy address. `http(s)` only; anything else is ignored |
194
194
  | `texts.detailsAction` | `"policy" \| "settings" \| "hide" \| "declaration"` | see notes | v0.5.0, `declaration` in v0.5.7. What «Learn more» does. Defaults to `policy` when `policyUrl` is set, `settings` when it is not. `policy` or `declaration` without a usable URL falls back to `settings` rather than rendering a dead link |
195
195
  | `texts.declarationUrl` | `string` | — | v0.5.7. Address of the cookie declaration page, used by `detailsAction: "declaration"`. `http(s)` only. Filled by the hosted service; the client only reads it |
196
+ | `texts.<lang>` | `object` | — | v0.5.15. Per-language dictionary overrides, keyed by a language tag (`ru`, `ro`, `en`, `pt-br`, …). Overridable keys: `bannerTitle`, `bannerText`, `panelTitle`, `panelIntro`, `extraTitle`, `extraText`, and `cat.<necessary\|functional\|analytics\|marketing>.title` / `.desc`. An empty string falls through to the standard text; every other key is ignored — see [Custom texts and links](#custom-texts-and-links) |
197
+ | `texts.links` | `object[]` | `[]` | v0.5.15. Up to 3 links under the banner buttons and at the foot of the settings panel: `{ id, url, label: { ru, ro, en, … } }`. `url` is `http(s)` only; a row with no resolvable label or an unusable address is skipped. Does not affect `detailsAction`, except that «Learn more» is hidden when its `policy` / `declaration` URL repeats one of these links — see [Custom texts and links](#custom-texts-and-links) |
196
198
  | `categories.*.enabled` | `boolean` | `true` | Per category: `functional`, `analytics`, `marketing`. Hides the toggle when `false` |
197
199
  | `consentTtlDays` | `number` | `365` | Lifetime of the stored decision |
198
200
  | `integrations.gcm` | `boolean` | `true` | Google Consent Mode v2 signals |
@@ -363,10 +365,161 @@ Both link forms accept `http(s)` addresses only. A `javascript:` or `data:` URL
363
365
  in a control the visitor is invited to click is an XSS vector, so anything else
364
366
  is refused and the link degrades to `settings`.
365
367
 
368
+ Since 0.5.15 both link forms are also dropped for a render whose `texts.links`
369
+ already show the same address, so the banner never links one page twice — see
370
+ [`texts.links`](#textslinks--your-own-links).
371
+
366
372
  > Before 0.5.0 this control was rendered as `<a href="#">` with no handler at
367
373
  > all: clicking it jumped to the top of the page and nothing else. Any site
368
374
  > running 0.4.x or earlier has a dead «Learn more» link.
369
375
 
376
+ ### Custom texts and links
377
+
378
+ Since 0.5.15 the banner and the panel can carry your own words, per language,
379
+ and up to three of your own links. This exists because a cookie banner in some
380
+ jurisdictions has to name the operator and say where a data subject may
381
+ complain — that is a legal requirement, not decoration, and it does not fit in
382
+ any of the built-in sentences.
383
+
384
+ #### `texts.<lang>` — dictionary overrides
385
+
386
+ A key under `texts` is treated as a language dictionary when — and only when —
387
+ it looks like a language tag, matching `/^[a-z]{2}(-[a-z]{2})?$/`. That is what
388
+ keeps `policyUrl`, `detailsAction`, `declarationUrl` and `links` out of it; no
389
+ scalar setting under `texts` is ever two letters.
390
+
391
+ Overridable keys, and nothing else:
392
+
393
+ | Key | Where it shows |
394
+ |---|---|
395
+ | `bannerTitle` | The banner's heading |
396
+ | `bannerText` | The banner's paragraph |
397
+ | `panelTitle` | The settings panel's heading |
398
+ | `panelIntro` | The line under it |
399
+ | `extraTitle` | Heading of the «Additional information» block (defaults to a translated «Additional information» in all 32 languages) |
400
+ | `extraText` | Body of that block. **Empty in every dictionary** — the block renders only when you supply text |
401
+ | `cat.<name>.title` / `.desc` | One category's name and description, for `necessary`, `functional`, `analytics`, `marketing` |
402
+
403
+ Anything else — `acceptAll`, `more`, `save`, an unknown key — is ignored. The
404
+ button labels are what a visitor recognises across sites, and the plural tables
405
+ are arrays that a string override would break.
406
+
407
+ Values are merged in three layers, in this order, each winning over the one
408
+ before it:
409
+
410
+ ```
411
+ built-in dictionary ← window.__ckLocales ← config.texts[lang]
412
+ ```
413
+
414
+ For the last layer the exact resolved code is tried first, then its two-letter
415
+ base: a banner that resolved to `pt-br` reads `texts['pt-br']`, then
416
+ `texts['pt']`. **A non-empty string wins; an empty string, a missing key or a
417
+ non-string falls through to the layer below.** That is what makes an empty field
418
+ in an editor mean «use the standard text» rather than «show nothing».
419
+
420
+ Every value is treated as **text, never as HTML**, everywhere.
421
+
422
+ #### The markup subset for `extraText`
423
+
424
+ `extraText` is the one field with structure, because an operator block genuinely
425
+ is two or three paragraphs with an address and a link in them. The rules below
426
+ are the whole contract — the hosted service's validator mirrors them exactly,
427
+ so what the cabinet previews is what the banner paints:
428
+
429
+ 1. **Paragraphs.** A blank line (two newlines) starts a new paragraph. Each
430
+ paragraph becomes one `<p>`.
431
+ 2. **Line breaks.** A single newline inside a paragraph becomes a `<br>`.
432
+ 3. **Three passes, in this exact order.** The order is part of the contract,
433
+ not an implementation detail:
434
+ 1. **`[label](url)` links.** Tokenised first, and their pieces are never
435
+ seen by the later passes. This is what stops the bare-URL rule from
436
+ eating the address inside `[label](https://…)`, stops the bare-e-mail
437
+ rule from firing inside a `mailto:` label, and leaves a `**` inside a
438
+ link label literal.
439
+ 2. **`**bold**`**, over the text between those links.
440
+ 3. **Bare addresses**, inside each bold and each plain span: a bare
441
+ `https://` or `http://` URL, and a bare e-mail address, each becoming its
442
+ own label.
443
+ Bold must come *before* the bare addresses, not after: `**mail@example.md**`
444
+ is one bold run that happens to contain an address, and linking the address
445
+ first would split the run and leave the asterisks visible on screen.
446
+ 4. **What the passes produce.** Every link — `[label](https://…)`,
447
+ `[label](mailto:…)`, a bare URL, a bare e-mail — is rendered with
448
+ `target="_blank" rel="noopener"`. `**bold**` becomes `<strong>`, and an
449
+ address inside it is bold *and* clickable. There is no nesting the other
450
+ way: `**` inside a `[label](…)` stays literal, because pass 1 removed the
451
+ whole link before pass 2 ran. An unclosed `**` is two literal asterisks,
452
+ never a bold tail that swallows the paragraph.
453
+ 5. **Any other scheme is not a link.** `[x](javascript:…)`, `data:`, `file:` —
454
+ the *whole literal* `[x](javascript:…)` is rendered as plain text, so a
455
+ mistake is visible to whoever wrote it rather than silently swallowed.
456
+ 6. **No HTML.** `<b>` is four characters of text. Nothing in this path goes
457
+ through `innerHTML`; the block is built with `createElement` and
458
+ `createTextNode` only. The hosted service refuses `<` in these fields
459
+ outright, at validation time.
460
+
461
+ The block is drawn after the categories and their service lists, before the
462
+ panel's buttons, and only when `extraText` resolves to a non-empty string.
463
+
464
+ #### `texts.links` — your own links
465
+
466
+ Up to three, rendered as a row under the banner's buttons (in all three
467
+ layouts) and at the foot of the settings panel:
468
+
469
+ ```js
470
+ texts: {
471
+ links: [
472
+ { id: 'privacy', url: 'https://shop.md/privacy',
473
+ label: { ru: 'Политика конфиденциальности', ro: 'Politica de confidențialitate', en: 'Privacy policy' } },
474
+ { id: 'cookies', url: 'https://shop.md/cookies',
475
+ label: { ru: 'Политика cookie', ro: 'Politica cookie', en: 'Cookie policy' } }
476
+ ]
477
+ }
478
+ ```
479
+
480
+ `url` must be `http(s)`; anything else is skipped. `label` is resolved with the
481
+ same fallback chain as `branding.poweredBy.texts` — exact code, then the
482
+ two-letter base, then `en` — and a row whose label resolves to nothing is
483
+ skipped rather than rendered blank. The cap of three is applied to the rows that
484
+ *survive* those checks, so one malformed entry never costs a good one its place.
485
+
486
+ This does not touch `detailsAction`, with one exception that keeps the banner
487
+ from printing the same address twice: when `detailsAction` resolves to `policy`
488
+ or `declaration` and its URL matches one of the links on screen — compared
489
+ trimmed, with a case-insensitive host and any trailing slash ignored — the
490
+ in-text «Learn more» link is not rendered and the banner behaves as
491
+ `detailsAction: "hide"` for that render, leaving the address to the link row,
492
+ which also carries your own label for it. An explicit `detailsAction:
493
+ "settings"` is unaffected: it opens the panel and has no address to duplicate.
494
+
495
+ #### A worked example
496
+
497
+ ```json
498
+ {
499
+ "language": "auto",
500
+ "texts": {
501
+ "policyUrl": "https://shop.md/privacy",
502
+ "ru": {
503
+ "bannerTitle": "Cookie на сайте INTERSTEPCOM",
504
+ "extraText": "Оператор: «FIRM» SRL, IDNO 1234567890123, мун. Кишинёв, ул. Примерная 1.\n\nПо вопросам обработки персональных данных пишите на **office@firm.md** — отвечаем не позднее одного месяца (ст. 12 ч. (3) Закона № 195/2024).\n\nВы вправе подать жалобу в [Национальный центр по защите персональных данных](https://datepersonale.md)."
505
+ },
506
+ "links": [
507
+ { "id": "privacy", "url": "https://shop.md/privacy",
508
+ "label": { "ru": "Политика конфиденциальности", "ro": "Politica de confidențialitate" } },
509
+ { "id": "cookies", "url": "https://shop.md/cookies",
510
+ "label": { "ru": "Политика cookie", "ro": "Politica cookie" } }
511
+ ]
512
+ }
513
+ }
514
+ ```
515
+
516
+ A Russian visitor sees the custom banner title, and a panel whose «Дополнительно»
517
+ card carries three paragraphs: the operator's details, a bold auto-linked
518
+ address with the statutory answering period, and a link to the supervisory
519
+ authority. A Romanian visitor sees the standard Romanian banner title (nothing
520
+ was overridden for `ro`), no «Informații suplimentare» card (no `ro.extraText`),
521
+ and both links under the buttons in Romanian.
522
+
370
523
  ### Reopening the settings
371
524
 
372
525
  `ConsentKit.openSettings()` opens the preferences panel from anywhere on the
@@ -819,19 +972,19 @@ external requests. Rebuild them with `tools/build-inline.mjs` (see
819
972
  [`tools/README.md`](https://github.com/vermoh/ConsentKit/blob/main/tools/README.md)); each block's header records the exact
820
973
  command that produced it.
821
974
 
822
- ConsentKit 0.5.13, rebuilt 2026-09-07, uncompressed — gzip on the server cuts
975
+ ConsentKit 0.5.15, rebuilt 2026-09-07, uncompressed — gzip on the server cuts
823
976
  this roughly threefold. Every block includes the branding extension and the
824
977
  attribution line; `--no-branding` drops both the code and the config and takes
825
978
  **~26 KB** back off:
826
979
 
827
980
  | Block | Languages | Bytes | gzip | `--no-branding` |
828
981
  |---|---|---|---|---|
829
- | `ready/en-bar.txt` | en | 264,816 | 82,798 | 238,438 |
830
- | `ready/ru-bar.txt` | ru, ro, en | 266,806 | 83,673 | 240,242 |
831
- | `ready/ru-box.txt` | ru, ro, en | 266,821 | 83,682 | 240,257 |
832
- | `ready/ru-box-right.txt` | ru, ro, en | 266,830 | 83,687 | 240,266 |
833
- | `ready/ru-modal.txt` | ru, ro, en | 266,814 | 83,679 | 240,250 |
834
- | `ready/eu-bar.txt` | 34 languages | 315,460 | 101,998 | 288,916 |
982
+ | `ready/en-bar.txt` | en | 275,509 | 86,249 | 249,096 |
983
+ | `ready/ru-bar.txt` | ru, ro, en | 277,578 | 87,146 | 250,979 |
984
+ | `ready/ru-box.txt` | ru, ro, en | 277,593 | 87,156 | 250,994 |
985
+ | `ready/ru-box-right.txt` | ru, ro, en | 277,602 | 87,161 | 250,997 |
986
+ | `ready/ru-modal.txt` | ru, ro, en | 277,586 | 87,153 | 250,985 |
987
+ | `ready/eu-bar.txt` | 34 languages | 326,232 | 105,534 | 299,653 |
835
988
 
836
989
  The blocks are dominated by the core and the UI (roughly 97 KB and 129 KB of
837
990
  source respectively, comments included — the builder concatenates the sources
@@ -1023,6 +1176,47 @@ Client versions. The WordPress plugin tracks the same numbers and keeps its own
1023
1176
  notes in
1024
1177
  [`plugins/wordpress/consentkit/readme.txt`](https://github.com/vermoh/ConsentKit/blob/main/plugins/wordpress/consentkit/readme.txt).
1025
1178
 
1179
+ ### 0.5.15
1180
+
1181
+ - **Your own texts, per language.** `texts.<lang>` overrides the banner title
1182
+ and copy, the panel title and intro, and any category's title or description,
1183
+ for one language at a time. An empty field means «take the standard text», so
1184
+ a partly filled form is not a partly blank banner — see
1185
+ [Custom texts and links](#custom-texts-and-links).
1186
+ - **An «Additional information» block in the settings panel.** `texts.<lang>.extraText`
1187
+ renders as a card under the categories: who the operator is, where to write,
1188
+ how long an answer takes, where to complain. It accepts a small, precisely
1189
+ defined markup subset — paragraphs, `**bold**`, links and auto-linked e-mail
1190
+ addresses — and no HTML at all: the block is built with `createElement` and
1191
+ `createTextNode`, never `innerHTML`.
1192
+ - **Up to three of your own links under the banner buttons** and at the foot of
1193
+ the settings panel, via `texts.links`. Each carries a per-language label, is
1194
+ `http(s)`-only, and opens with `target="_blank" rel="noopener"`. «Learn more»
1195
+ is unaffected: a site that adds links keeps whatever `detailsAction` already
1196
+ did — except that the in-text link is dropped when it would repeat an address
1197
+ already on screen in the link row, so the same page is never linked twice.
1198
+ - **The debug panel reports which language got overrides**, so «I filled in the
1199
+ text and the banner still shows the standard one» has an answer on screen —
1200
+ usually that the override is filed under a language code the banner did not
1201
+ resolve to.
1202
+
1203
+ ### 0.5.14
1204
+
1205
+ - **The loader tolerates a second snippet with a dead site id.** A page that
1206
+ carries two `data-ck-id` blocks — a migration that left the old one in place —
1207
+ is driven by the snippet whose config loads; the failing one only warns. The
1208
+ strict fallback is raised solely when *every* snippet fails, never while
1209
+ another is still in flight. The same id twice (Tilda duplicates the head
1210
+ block) initialises once and warns about nothing.
1211
+ - **A second copy of the script on the same page no longer replaces the first.**
1212
+ Each snippet loads the whole bundle, so a two-snippet page ran the client
1213
+ twice: the second core published a fresh, uninitialised engine over
1214
+ `window.ConsentKit` and the second UI layer mounted its own banner, leaving
1215
+ the real config applied to an engine nothing pointed at any more — a banner
1216
+ drawn from the built-in defaults. The second copy now stands down: the first
1217
+ engine keeps its state, its DOM patches and its observer, and the page mounts
1218
+ exactly one banner.
1219
+
1026
1220
  ### 0.5.13
1027
1221
 
1028
1222
  - Host database: `fbcdn.net` → marketing (Facebook SDK chunks and plugin images); `aichat.md`, `bubble.aichat.md` → functional (chat widget); `challenges.cloudflare.com` (Cloudflare challenge / Turnstile) and `i.imgur.com` → infrastructure, never held.
@@ -1143,7 +1337,7 @@ notes in
1143
1337
 
1144
1338
  ## Project status
1145
1339
 
1146
- **This is a prototype (v0.5.13), not a released product.** It is honest about
1340
+ **This is a prototype (v0.5.15), not a released product.** It is honest about
1147
1341
  what has been verified and what has not.
1148
1342
 
1149
1343
  ### Verified
package/npm/index.d.ts CHANGED
@@ -168,6 +168,40 @@ export interface CkTextsConfig {
168
168
  * service; the client only reads it.
169
169
  */
170
170
  declarationUrl?: string;
171
+ /**
172
+ * v0.5.15. Up to three links rendered under the banner buttons and at the
173
+ * bottom of the preferences panel. `url` is `http(s)` only; `label` is
174
+ * resolved per language the way `branding.poweredBy.texts` is. A link whose
175
+ * address equals the «Learn more» target hides the in-text link.
176
+ */
177
+ links?: CkTextLink[];
178
+ /**
179
+ * v0.5.15. Per-language overrides of the built-in dictionary, keyed by a
180
+ * language code (`ru`, `ro`, `en`, `pt-br`, …). An empty string means «use
181
+ * the dictionary». `extraText` accepts the rich-text subset documented in
182
+ * the README («Custom texts and links»); it is never interpreted as HTML.
183
+ */
184
+ [lang: string]: CkTextOverrides | CkTextLink[] | string | undefined;
185
+ }
186
+
187
+ /** v0.5.15. One link under the banner buttons. */
188
+ export interface CkTextLink {
189
+ id: string;
190
+ url: string;
191
+ label: Record<string, string>;
192
+ }
193
+
194
+ /** v0.5.15. The dictionary keys a site may override for one language. */
195
+ export interface CkTextOverrides {
196
+ bannerTitle?: string;
197
+ bannerText?: string;
198
+ panelTitle?: string;
199
+ panelIntro?: string;
200
+ /** Heading of the «Additional information» block in the preferences panel. */
201
+ extraTitle?: string;
202
+ /** Body of that block; rendered only when non-empty. Rich-text subset. */
203
+ extraText?: string;
204
+ cat?: Partial<Record<'necessary' | 'functional' | 'analytics' | 'marketing', { title?: string; desc?: string }>>;
171
205
  }
172
206
 
173
207
  /** Whether a category is offered in the preferences panel at all. */
@@ -421,7 +455,7 @@ export interface CkContrastApi {
421
455
  resolveFont(theme: CkThemeConfig): string;
422
456
  /** Takes the whole config, not just `texts`. */
423
457
  resolveDetails(config: CkConfig):
424
- { kind: 'policy' | 'settings' | 'hide'; href: string | null };
458
+ { kind: 'policy' | 'settings' | 'hide' | 'declaration'; href: string | null };
425
459
  /** Takes the whole config. Returns the generated stylesheet and both palettes. */
426
460
  buildThemeCss(config: CkConfig): {
427
461
  css: string;
@@ -28,7 +28,7 @@ export function undecidedState() {
28
28
  */
29
29
  export function createStub() {
30
30
  const stub = {
31
- version: '0.5.13',
31
+ version: '0.5.15',
32
32
  config: {},
33
33
  init: function () { return undecidedState(); },
34
34
  allowed: function (cat) { return cat === 'necessary'; },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomconsult/consentkit",
3
- "version": "0.5.13",
3
+ "version": "0.5.15",
4
4
  "description": "GDPR cookie consent core with blocking engine, Shadow DOM UI and Google Consent Mode v2. Zero dependencies, no build step.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/ck-core.js CHANGED
@@ -9,6 +9,29 @@
9
9
  'use strict';
10
10
 
11
11
  if (!global) { return; }
12
+
13
+ /* SPEC §1.9 — a second copy of ck.js on the page stands down.
14
+ A page can carry two snippets: an old page-level block with a dead site id
15
+ that nobody ever removed, plus the site-wide one. Each <script> loads the
16
+ WHOLE bundle, so this file runs twice — and without this guard the second
17
+ run would publish a fresh, uninitialised engine over `window.ConsentKit`.
18
+ Everything the first engine did stays behind on an object nothing points at
19
+ any more: its consent state, and the loader that is mid-fetch holding a
20
+ `CK` reference to it. The page would then end on the default config with a
21
+ banner drawn from default texts, while the real config was applied to the
22
+ orphan.
23
+ Standing down here is also what the two loaders' arbitration in ck-saas.js
24
+ assumes: `__ckSaas` coordinates them on the premise that both are talking
25
+ to the SAME core, so `shared.done` genuinely means "this page is
26
+ initialised" rather than "some object was initialised".
27
+ The patches, the MutationObserver and the initial scan are already live
28
+ from the first run; re-installing them would double every interception.
29
+ `init` is the test rather than mere presence, because ck-ui-branding.js
30
+ pre-creates a bare `window.ConsentKit = {}` when it happens to load first.
31
+ That object has no `init`, so we fall through and publish over it exactly
32
+ as before. */
33
+ if (global.ConsentKit && typeof global.ConsentKit.init === 'function') { return; }
34
+
12
35
  var doc = global.document;
13
36
 
14
37
  // ---------------------------------------------------------------------------
@@ -1978,7 +2001,7 @@
1978
2001
  // Public API
1979
2002
  // ---------------------------------------------------------------------------
1980
2003
  var ConsentKit = {
1981
- version: '0.5.13',
2004
+ version: '0.5.15',
1982
2005
  config: config,
1983
2006
 
1984
2007
  init: function (userConfig) {
package/src/ck-debug.js CHANGED
@@ -272,6 +272,7 @@
272
272
  source: 'источник',
273
273
  srcSaas: 'SaaS',
274
274
  srcInline: 'инлайн',
275
+ twoSnippets: 'два снипета',
275
276
  secConsent: 'Согласие',
276
277
  decidedAt: 'решение',
277
278
  method: 'способ',
@@ -311,6 +312,12 @@
311
312
  themeCard: 'карточка',
312
313
  themeBtn: 'кнопки',
313
314
  themeLink: 'Ссылки',
315
+ // SPEC V1.16 §1.4 — какой язык получил свои тексты из конфига. Отвечает
316
+ // на вопрос «почему на баннере не тот текст, который я вписал»: чаще
317
+ // всего потому, что переопределение записано под другим кодом языка.
318
+ textsOverride: 'Тексты',
319
+ textsFor: 'переопределения для ',
320
+ textsNone: 'нет',
314
321
  btnAccept: 'Принять всё',
315
322
  btnReject: 'Отклонить всё',
316
323
  btnSettings: 'Настроить',
@@ -348,6 +355,7 @@
348
355
  source: 'source',
349
356
  srcSaas: 'SaaS',
350
357
  srcInline: 'inline',
358
+ twoSnippets: 'two snippets',
351
359
  secConsent: 'Consent',
352
360
  decidedAt: 'decided',
353
361
  method: 'method',
@@ -385,6 +393,9 @@
385
393
  themeCard: 'card',
386
394
  themeBtn: 'buttons',
387
395
  themeLink: 'Links',
396
+ textsOverride: 'Texts',
397
+ textsFor: 'overrides for ',
398
+ textsNone: 'none',
388
399
  btnAccept: 'Accept all',
389
400
  btnReject: 'Reject all',
390
401
  btnSettings: 'Customize',
@@ -423,6 +434,7 @@
423
434
  source: 'sursă',
424
435
  srcSaas: 'SaaS',
425
436
  srcInline: 'inline',
437
+ twoSnippets: 'două fragmente',
426
438
  secConsent: 'Consimțământ',
427
439
  decidedAt: 'decizie',
428
440
  method: 'mod',
@@ -460,6 +472,9 @@
460
472
  themeCard: 'card',
461
473
  themeBtn: 'butoane',
462
474
  themeLink: 'Linkuri',
475
+ textsOverride: 'Texte',
476
+ textsFor: 'suprascrieri pentru ',
477
+ textsNone: 'niciuna',
463
478
  btnAccept: 'Acceptă tot',
464
479
  btnReject: 'Respinge tot',
465
480
  btnSettings: 'Personalizează',
@@ -850,6 +865,45 @@
850
865
  : 'inherit (' + T.themeFontInherit + tries + ')';
851
866
  }
852
867
 
868
+ /* SPEC V1.16 §1.4 — «Тексты: переопределения для ru» / «нет».
869
+
870
+ The question this answers is «I filled the banner text in the cabinet and
871
+ the banner still shows the standard one». Nine times in ten the answer is
872
+ that the override is filed under a language code the banner did not resolve
873
+ to — so the row reports the RESOLVED code, not the configured one, and says
874
+ which of the two lookups (exact, then two-letter base) actually hit.
875
+
876
+ The resolution is asked of ck-ui through _contrast rather than repeated
877
+ here: «один код — одни числа», the same rule the Appearance section above
878
+ already follows. With ck-ui absent (a core-only page) there is no banner
879
+ language to report and the row reads «нет». */
880
+ function textsRow(T) {
881
+ var cfg = (CK && CK.config) || {};
882
+ var texts = (cfg.texts && typeof cfg.texts === 'object' && !Array.isArray(cfg.texts))
883
+ ? cfg.texts : null;
884
+ if (!texts) return T.textsNone;
885
+ var lang = '';
886
+ try {
887
+ var C = CK && CK._contrast;
888
+ if (C && typeof C.resolveLang === 'function' && typeof C.localeTable === 'function') {
889
+ lang = String(C.resolveLang(cfg.language, C.localeTable()) || '').toLowerCase();
890
+ }
891
+ } catch (e) { lang = ''; }
892
+ if (!lang) return T.textsNone;
893
+
894
+ function has(k) {
895
+ return !!(k && /^[a-z]{2}(-[a-z]{2})?$/.test(k) &&
896
+ Object.prototype.hasOwnProperty.call(texts, k) &&
897
+ texts[k] && typeof texts[k] === 'object' && !Array.isArray(texts[k]));
898
+ }
899
+ // The exact code first, then its two-letter base — the same order
900
+ // buildStrings() merges them in, so the row names the key that actually won.
901
+ if (has(lang)) return T.textsFor + lang;
902
+ var base = lang.slice(0, 2);
903
+ if (base !== lang && has(base)) return T.textsFor + base;
904
+ return T.textsNone;
905
+ }
906
+
853
907
  // Resolved lazily, not at parse time: this file runs before ConsentKit.init()
854
908
  // has merged the site's config, so asking for the language now would always
855
909
  // read the built-in default. Re-resolved on every render so a page that
@@ -910,14 +964,26 @@
910
964
  body.textContent = '';
911
965
 
912
966
  // 1. Client
967
+ /* Read straight off _saas rather than through buildReport(): more than one
968
+ snippet on a page is a fact about the page, not about the report, and it
969
+ is worth surfacing because the symptom it explains — a banner configured
970
+ by an id the owner did not expect — is otherwise invisible. */
971
+ var siteIds = null;
972
+ try {
973
+ var ids = CK && CK._saas && CK._saas.siteIds;
974
+ if (ids && ids.length > 1) { siteIds = ids.join(', '); }
975
+ } catch (e) { siteIds = null; }
976
+
913
977
  var s1 = section(T.secClient);
914
- s1.appendChild(defs([
978
+ var rows1 = [
915
979
  [T.version, r.client.version],
916
980
  [T.source, r.client.source === 'saas' ? T.srcSaas : T.srcInline],
917
- ['siteId', r.client.siteId],
918
- ['policyVersion', r.client.policyVersion],
919
- ['ETag', r.client.etag]
920
- ]));
981
+ ['siteId', r.client.siteId]
982
+ ];
983
+ if (siteIds) { rows1.push([T.twoSnippets, siteIds]); }
984
+ rows1.push(['policyVersion', r.client.policyVersion]);
985
+ rows1.push(['ETag', r.client.etag]);
986
+ s1.appendChild(defs(rows1));
921
987
  body.appendChild(s1);
922
988
 
923
989
  // 2. Consent
@@ -1061,7 +1127,9 @@
1061
1127
  [T.themeFont, fontRow(built, T)],
1062
1128
  [T.themeRadius, T.themeCard + ' ' + built.radius.card + 'px · ' +
1063
1129
  T.themeBtn + ' ' + built.radius.button + 'px'],
1064
- [T.themeLink, lkTxt]
1130
+ [T.themeLink, lkTxt],
1131
+ // SPEC V1.16 §1.4 — «Тексты: переопределения для ru» / «нет».
1132
+ [T.textsOverride, textsRow(T)]
1065
1133
  ]));
1066
1134
 
1067
1135
  var labels = { accept: T.btnAccept, reject: T.btnReject, settings: T.btnSettings };