@ecomconsult/consentkit 0.5.14 → 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 +180 -3
- package/npm/index.d.ts +35 -1
- package/npm/internal-stub.mjs +1 -1
- package/package.json +1 -1
- package/src/ck-core.js +1 -1
- package/src/ck-debug.js +54 -1
- package/src/ck-locales.js +36 -1
- package/src/ck-ui.js +537 -6
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.
|
|
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,7 +972,7 @@ 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.
|
|
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:
|
|
@@ -1023,6 +1176,30 @@ 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
|
+
|
|
1026
1203
|
### 0.5.14
|
|
1027
1204
|
|
|
1028
1205
|
- **The loader tolerates a second snippet with a dead site id.** A page that
|
|
@@ -1160,7 +1337,7 @@ notes in
|
|
|
1160
1337
|
|
|
1161
1338
|
## Project status
|
|
1162
1339
|
|
|
1163
|
-
**This is a prototype (v0.5.
|
|
1340
|
+
**This is a prototype (v0.5.15), not a released product.** It is honest about
|
|
1164
1341
|
what has been verified and what has not.
|
|
1165
1342
|
|
|
1166
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;
|
package/npm/internal-stub.mjs
CHANGED
|
@@ -28,7 +28,7 @@ export function undecidedState() {
|
|
|
28
28
|
*/
|
|
29
29
|
export function createStub() {
|
|
30
30
|
const stub = {
|
|
31
|
-
version: '0.5.
|
|
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
package/src/ck-core.js
CHANGED
package/src/ck-debug.js
CHANGED
|
@@ -312,6 +312,12 @@
|
|
|
312
312
|
themeCard: 'карточка',
|
|
313
313
|
themeBtn: 'кнопки',
|
|
314
314
|
themeLink: 'Ссылки',
|
|
315
|
+
// SPEC V1.16 §1.4 — какой язык получил свои тексты из конфига. Отвечает
|
|
316
|
+
// на вопрос «почему на баннере не тот текст, который я вписал»: чаще
|
|
317
|
+
// всего потому, что переопределение записано под другим кодом языка.
|
|
318
|
+
textsOverride: 'Тексты',
|
|
319
|
+
textsFor: 'переопределения для ',
|
|
320
|
+
textsNone: 'нет',
|
|
315
321
|
btnAccept: 'Принять всё',
|
|
316
322
|
btnReject: 'Отклонить всё',
|
|
317
323
|
btnSettings: 'Настроить',
|
|
@@ -387,6 +393,9 @@
|
|
|
387
393
|
themeCard: 'card',
|
|
388
394
|
themeBtn: 'buttons',
|
|
389
395
|
themeLink: 'Links',
|
|
396
|
+
textsOverride: 'Texts',
|
|
397
|
+
textsFor: 'overrides for ',
|
|
398
|
+
textsNone: 'none',
|
|
390
399
|
btnAccept: 'Accept all',
|
|
391
400
|
btnReject: 'Reject all',
|
|
392
401
|
btnSettings: 'Customize',
|
|
@@ -463,6 +472,9 @@
|
|
|
463
472
|
themeCard: 'card',
|
|
464
473
|
themeBtn: 'butoane',
|
|
465
474
|
themeLink: 'Linkuri',
|
|
475
|
+
textsOverride: 'Texte',
|
|
476
|
+
textsFor: 'suprascrieri pentru ',
|
|
477
|
+
textsNone: 'niciuna',
|
|
466
478
|
btnAccept: 'Acceptă tot',
|
|
467
479
|
btnReject: 'Respinge tot',
|
|
468
480
|
btnSettings: 'Personalizează',
|
|
@@ -853,6 +865,45 @@
|
|
|
853
865
|
: 'inherit (' + T.themeFontInherit + tries + ')';
|
|
854
866
|
}
|
|
855
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
|
+
|
|
856
907
|
// Resolved lazily, not at parse time: this file runs before ConsentKit.init()
|
|
857
908
|
// has merged the site's config, so asking for the language now would always
|
|
858
909
|
// read the built-in default. Re-resolved on every render so a page that
|
|
@@ -1076,7 +1127,9 @@
|
|
|
1076
1127
|
[T.themeFont, fontRow(built, T)],
|
|
1077
1128
|
[T.themeRadius, T.themeCard + ' ' + built.radius.card + 'px · ' +
|
|
1078
1129
|
T.themeBtn + ' ' + built.radius.button + 'px'],
|
|
1079
|
-
[T.themeLink, lkTxt]
|
|
1130
|
+
[T.themeLink, lkTxt],
|
|
1131
|
+
// SPEC V1.16 §1.4 — «Тексты: переопределения для ru» / «нет».
|
|
1132
|
+
[T.textsOverride, textsRow(T)]
|
|
1080
1133
|
]));
|
|
1081
1134
|
|
|
1082
1135
|
var labels = { accept: T.btnAccept, reject: T.btnReject, settings: T.btnSettings };
|
package/src/ck-locales.js
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
+ uk tr no nb is sr ca sq mk
|
|
9
9
|
|
|
10
10
|
Key set mirrors the builtin en dictionary of ck-ui.js exactly:
|
|
11
|
-
19 flat strings + cat.{necessary,functional,analytics,marketing}.{title,desc}
|
|
11
|
+
19 flat strings + cat.{necessary,functional,analytics,marketing}.{title,desc},
|
|
12
|
+
plus `extraTitle` (SPEC V1.16 §1.2 — the heading of the «Дополнительно» card
|
|
13
|
+
in the settings panel). Its BODY, `extraText`, is deliberately not here: the
|
|
14
|
+
block renders only from what the site itself supplies, so a translated body
|
|
15
|
+
would put an empty operator card on every site that never configured one.
|
|
12
16
|
|
|
13
17
|
NOTE: these translations are drafts produced for the prototype. They MUST be reviewed
|
|
14
18
|
by native speakers before production use — legal wording (refusal, "always on") is the
|
|
@@ -40,6 +44,7 @@
|
|
|
40
44
|
colPurpose: 'Цел',
|
|
41
45
|
colExpiry: 'Изтича',
|
|
42
46
|
floating: 'Настройки на бисквитките',
|
|
47
|
+
extraTitle: 'Допълнителна информация',
|
|
43
48
|
cat: {
|
|
44
49
|
necessary: { title: 'Необходими', desc: 'Нужни са, за да работи сайтът — вход, сигурност, запомняне на съгласието ви. Не могат да се изключат.' },
|
|
45
50
|
functional: { title: 'Функционални', desc: 'Запомнят предпочитанията ви, например език или чат, за да не ги задавате отново.' },
|
|
@@ -68,6 +73,7 @@
|
|
|
68
73
|
colPurpose: 'Účel',
|
|
69
74
|
colExpiry: 'Platnost',
|
|
70
75
|
floating: 'Nastavení cookies',
|
|
76
|
+
extraTitle: 'Doplňující informace',
|
|
71
77
|
cat: {
|
|
72
78
|
necessary: { title: 'Nezbytné', desc: 'Bez nich web nefunguje — přihlášení, bezpečnost, uložení vašeho souhlasu. Nelze je vypnout.' },
|
|
73
79
|
functional: { title: 'Funkční', desc: 'Pamatují si vaše předvolby, třeba jazyk nebo chat, abyste je nenastavovali znovu.' },
|
|
@@ -96,6 +102,7 @@
|
|
|
96
102
|
colPurpose: 'Formål',
|
|
97
103
|
colExpiry: 'Udløber',
|
|
98
104
|
floating: 'Cookieindstillinger',
|
|
105
|
+
extraTitle: 'Yderligere oplysninger',
|
|
99
106
|
cat: {
|
|
100
107
|
necessary: { title: 'Nødvendige', desc: 'Nødvendige for at siden virker — login, sikkerhed, hukommelse om dit samtykke. De kan ikke slås fra.' },
|
|
101
108
|
functional: { title: 'Funktionelle', desc: 'Husker dine indstillinger, fx sprog eller chat, så du ikke skal vælge dem igen.' },
|
|
@@ -124,6 +131,7 @@
|
|
|
124
131
|
colPurpose: 'Zweck',
|
|
125
132
|
colExpiry: 'Laufzeit',
|
|
126
133
|
floating: 'Cookie-Einstellungen',
|
|
134
|
+
extraTitle: 'Weitere Informationen',
|
|
127
135
|
cat: {
|
|
128
136
|
necessary: { title: 'Notwendig', desc: 'Nötig, damit die Website funktioniert — Anmeldung, Sicherheit, Speichern Ihrer Einwilligung. Sie lassen sich nicht abschalten.' },
|
|
129
137
|
functional: { title: 'Funktional', desc: 'Merken sich Ihre Einstellungen, etwa Sprache oder Chat, damit Sie sie nicht erneut wählen müssen.' },
|
|
@@ -152,6 +160,7 @@
|
|
|
152
160
|
colPurpose: 'Σκοπός',
|
|
153
161
|
colExpiry: 'Λήξη',
|
|
154
162
|
floating: 'Ρυθμίσεις cookies',
|
|
163
|
+
extraTitle: 'Πρόσθετες πληροφορίες',
|
|
155
164
|
cat: {
|
|
156
165
|
necessary: { title: 'Απαραίτητα', desc: 'Χρειάζονται για να λειτουργεί ο ιστότοπος — σύνδεση, ασφάλεια, διατήρηση της επιλογής σας. Δεν απενεργοποιούνται.' },
|
|
157
166
|
functional: { title: 'Λειτουργικά', desc: 'Θυμούνται τις προτιμήσεις σας, όπως γλώσσα ή συνομιλία, ώστε να μην τις ορίζετε ξανά.' },
|
|
@@ -180,6 +189,7 @@
|
|
|
180
189
|
colPurpose: 'Finalidad',
|
|
181
190
|
colExpiry: 'Caduca',
|
|
182
191
|
floating: 'Configuración de cookies',
|
|
192
|
+
extraTitle: 'Información adicional',
|
|
183
193
|
cat: {
|
|
184
194
|
necessary: { title: 'Necesarias', desc: 'Imprescindibles para que el sitio funcione: inicio de sesión, seguridad y memoria de su consentimiento. No se pueden desactivar.' },
|
|
185
195
|
functional: { title: 'Funcionales', desc: 'Recuerdan sus preferencias, como el idioma o el chat, para que no tenga que configurarlas de nuevo.' },
|
|
@@ -208,6 +218,7 @@
|
|
|
208
218
|
colPurpose: 'Eesmärk',
|
|
209
219
|
colExpiry: 'Kehtivus',
|
|
210
220
|
floating: 'Küpsiste seaded',
|
|
221
|
+
extraTitle: 'Lisateave',
|
|
211
222
|
cat: {
|
|
212
223
|
necessary: { title: 'Vajalikud', desc: 'Vajalikud saidi tööks — sisselogimine, turvalisus, teie nõusoleku meelespidamine. Neid ei saa välja lülitada.' },
|
|
213
224
|
functional: { title: 'Funktsionaalsed', desc: 'Jätavad meelde teie eelistused, näiteks keele või vestluse, et te ei peaks neid uuesti valima.' },
|
|
@@ -236,6 +247,7 @@
|
|
|
236
247
|
colPurpose: 'Tarkoitus',
|
|
237
248
|
colExpiry: 'Voimassaolo',
|
|
238
249
|
floating: 'Evästeasetukset',
|
|
250
|
+
extraTitle: 'Lisätiedot',
|
|
239
251
|
cat: {
|
|
240
252
|
necessary: { title: 'Välttämättömät', desc: 'Tarvitaan sivuston toimintaan: kirjautuminen, turvallisuus ja suostumuksenne muistaminen. Näitä ei voi poistaa käytöstä.' },
|
|
241
253
|
functional: { title: 'Toiminnalliset', desc: 'Muistavat asetuksenne, kuten kielen tai chatin, jottei niitä tarvitse valita uudelleen.' },
|
|
@@ -264,6 +276,7 @@
|
|
|
264
276
|
colPurpose: 'Finalité',
|
|
265
277
|
colExpiry: 'Expiration',
|
|
266
278
|
floating: 'Paramètres des cookies',
|
|
279
|
+
extraTitle: 'Informations complémentaires',
|
|
267
280
|
cat: {
|
|
268
281
|
necessary: { title: 'Nécessaires', desc: 'Indispensables au fonctionnement du site : connexion, sécurité, mémorisation de votre choix. Ils ne peuvent pas être désactivés.' },
|
|
269
282
|
functional: { title: 'Fonctionnels', desc: 'Retiennent vos préférences, par exemple la langue ou le chat, pour ne pas avoir à les régler de nouveau.' },
|
|
@@ -292,6 +305,7 @@
|
|
|
292
305
|
colPurpose: 'Cuspóir',
|
|
293
306
|
colExpiry: 'Éagann',
|
|
294
307
|
floating: 'Socruithe fianán',
|
|
308
|
+
extraTitle: 'Eolas breise',
|
|
295
309
|
cat: {
|
|
296
310
|
necessary: { title: 'Riachtanach', desc: 'Ag teastáil chun an suíomh a oibriú — logáil isteach, slándáil, do thoiliú a choinneáil. Ní féidir iad a mhúchadh.' },
|
|
297
311
|
functional: { title: 'Feidhmiúil', desc: 'Coinníonn siad do roghanna, mar shampla an teanga nó an comhrá, ionas nach gá duit iad a shocrú arís.' },
|
|
@@ -320,6 +334,7 @@
|
|
|
320
334
|
colPurpose: 'Svrha',
|
|
321
335
|
colExpiry: 'Istječe',
|
|
322
336
|
floating: 'Postavke kolačića',
|
|
337
|
+
extraTitle: 'Dodatne informacije',
|
|
323
338
|
cat: {
|
|
324
339
|
necessary: { title: 'Nužni', desc: 'Potrebni su za rad stranice — prijava, sigurnost, pamćenje vaše privole. Ne mogu se isključiti.' },
|
|
325
340
|
functional: { title: 'Funkcionalni', desc: 'Pamte vaše postavke, primjerice jezik ili chat, da ih ne morate ponovno birati.' },
|
|
@@ -348,6 +363,7 @@
|
|
|
348
363
|
colPurpose: 'Cél',
|
|
349
364
|
colExpiry: 'Lejárat',
|
|
350
365
|
floating: 'Sütibeállítások',
|
|
366
|
+
extraTitle: 'További információk',
|
|
351
367
|
cat: {
|
|
352
368
|
necessary: { title: 'Szükséges', desc: 'Az oldal működéséhez kellenek: belépés, biztonság, a hozzájárulása megjegyzése. Nem kapcsolhatók ki.' },
|
|
353
369
|
functional: { title: 'Funkcionális', desc: 'Megjegyzik a beállításait, például a nyelvet vagy a chatet, hogy ne kelljen újra megadnia őket.' },
|
|
@@ -376,6 +392,7 @@
|
|
|
376
392
|
colPurpose: 'Finalità',
|
|
377
393
|
colExpiry: 'Scadenza',
|
|
378
394
|
floating: 'Impostazioni dei cookie',
|
|
395
|
+
extraTitle: 'Informazioni aggiuntive',
|
|
379
396
|
cat: {
|
|
380
397
|
necessary: { title: 'Necessari', desc: 'Servono al funzionamento del sito: accesso, sicurezza, memoria del suo consenso. Non possono essere disattivati.' },
|
|
381
398
|
functional: { title: 'Funzionali', desc: 'Ricordano le sue preferenze, come la lingua o la chat, così non deve impostarle di nuovo.' },
|
|
@@ -404,6 +421,7 @@
|
|
|
404
421
|
colPurpose: 'Paskirtis',
|
|
405
422
|
colExpiry: 'Galiojimas',
|
|
406
423
|
floating: 'Slapukų nustatymai',
|
|
424
|
+
extraTitle: 'Papildoma informacija',
|
|
407
425
|
cat: {
|
|
408
426
|
necessary: { title: 'Būtini', desc: 'Reikalingi, kad svetainė veiktų: prisijungimas, saugumas, jūsų sutikimo įsiminimas. Jų išjungti negalima.' },
|
|
409
427
|
functional: { title: 'Funkciniai', desc: 'Įsimena jūsų nustatymus, pavyzdžiui, kalbą ar pokalbius, kad jų nereikėtų nurodyti iš naujo.' },
|
|
@@ -432,6 +450,7 @@
|
|
|
432
450
|
colPurpose: 'Nolūks',
|
|
433
451
|
colExpiry: 'Derīgums',
|
|
434
452
|
floating: 'Sīkdatņu iestatījumi',
|
|
453
|
+
extraTitle: 'Papildu informācija',
|
|
435
454
|
cat: {
|
|
436
455
|
necessary: { title: 'Nepieciešamās', desc: 'Vajadzīgas vietnes darbībai — pieteikšanās, drošība, jūsu piekrišanas saglabāšana. Tās nevar izslēgt.' },
|
|
437
456
|
functional: { title: 'Funkcionālās', desc: 'Atceras jūsu iestatījumus, piemēram, valodu vai tērzēšanu, lai tie nebūtu jānorāda atkārtoti.' },
|
|
@@ -460,6 +479,7 @@
|
|
|
460
479
|
colPurpose: 'Skop',
|
|
461
480
|
colExpiry: 'Skadenza',
|
|
462
481
|
floating: 'Issettjar tal-cookies',
|
|
482
|
+
extraTitle: 'Informazzjoni addizzjonali',
|
|
463
483
|
cat: {
|
|
464
484
|
necessary: { title: 'Meħtieġa', desc: 'Meħtieġa biex is-sit jaħdem — login, sigurtà, tifkira tal-kunsens tiegħek. Ma jistgħux jintfew.' },
|
|
465
485
|
functional: { title: 'Funzjonali', desc: 'Jiftakru l-preferenzi tiegħek, bħal-lingwa jew iċ-chat, biex ma jkollokx terġa’ tissettjahom.' },
|
|
@@ -488,6 +508,7 @@
|
|
|
488
508
|
colPurpose: 'Doel',
|
|
489
509
|
colExpiry: 'Vervalt',
|
|
490
510
|
floating: 'Cookie-instellingen',
|
|
511
|
+
extraTitle: 'Aanvullende informatie',
|
|
491
512
|
cat: {
|
|
492
513
|
necessary: { title: 'Noodzakelijk', desc: 'Nodig om de site te laten werken: inloggen, beveiliging en het onthouden van uw keuze. Ze kunnen niet uit.' },
|
|
493
514
|
functional: { title: 'Functioneel', desc: 'Onthouden uw voorkeuren, zoals taal of chat, zodat u ze niet opnieuw hoeft in te stellen.' },
|
|
@@ -516,6 +537,7 @@
|
|
|
516
537
|
colPurpose: 'Cel',
|
|
517
538
|
colExpiry: 'Wygasa',
|
|
518
539
|
floating: 'Ustawienia plików cookie',
|
|
540
|
+
extraTitle: 'Informacje dodatkowe',
|
|
519
541
|
cat: {
|
|
520
542
|
necessary: { title: 'Niezbędne', desc: 'Potrzebne, by strona działała: logowanie, bezpieczeństwo, zapamiętanie zgody. Nie można ich wyłączyć.' },
|
|
521
543
|
functional: { title: 'Funkcjonalne', desc: 'Zapamiętują ustawienia, na przykład język lub czat, żeby nie trzeba było wybierać ich ponownie.' },
|
|
@@ -544,6 +566,7 @@
|
|
|
544
566
|
colPurpose: 'Finalidade',
|
|
545
567
|
colExpiry: 'Validade',
|
|
546
568
|
floating: 'Definições de cookies',
|
|
569
|
+
extraTitle: 'Informações adicionais',
|
|
547
570
|
cat: {
|
|
548
571
|
necessary: { title: 'Necessários', desc: 'Precisos para o site funcionar: início de sessão, segurança e memória do seu consentimento. Não podem ser desativados.' },
|
|
549
572
|
functional: { title: 'Funcionais', desc: 'Guardam as suas preferências, como o idioma ou o chat, para não ter de as definir outra vez.' },
|
|
@@ -572,6 +595,7 @@
|
|
|
572
595
|
colPurpose: 'Pentru ce',
|
|
573
596
|
colExpiry: 'Expiră',
|
|
574
597
|
floating: 'Setări cookie-uri',
|
|
598
|
+
extraTitle: 'Informații suplimentare',
|
|
575
599
|
// SPEC V1.10 §2 — заглушка встраивания. Only ro carries these here: en and
|
|
576
600
|
// ru are builtin in ck-ui.js, and every other locale falls back to en.
|
|
577
601
|
phText: 'Aici este conținut de la {host}. Se va încărca după acordul pentru «{cat}».',
|
|
@@ -616,6 +640,7 @@
|
|
|
616
640
|
colPurpose: 'Účel',
|
|
617
641
|
colExpiry: 'Platnosť',
|
|
618
642
|
floating: 'Nastavenia cookies',
|
|
643
|
+
extraTitle: 'Doplňujúce informácie',
|
|
619
644
|
cat: {
|
|
620
645
|
necessary: { title: 'Nevyhnutné', desc: 'Bez nich stránka nefunguje — prihlásenie, bezpečnosť, zapamätanie vášho súhlasu. Nedajú sa vypnúť.' },
|
|
621
646
|
functional: { title: 'Funkčné', desc: 'Pamätajú si vaše nastavenia, napríklad jazyk alebo chat, aby ste ich nezadávali znova.' },
|
|
@@ -644,6 +669,7 @@
|
|
|
644
669
|
colPurpose: 'Namen',
|
|
645
670
|
colExpiry: 'Poteče',
|
|
646
671
|
floating: 'Nastavitve piškotkov',
|
|
672
|
+
extraTitle: 'Dodatne informacije',
|
|
647
673
|
cat: {
|
|
648
674
|
necessary: { title: 'Nujni', desc: 'Potrebni za delovanje spletnega mesta — prijava, varnost, pomnjenje vašega soglasja. Ni jih mogoče izklopiti.' },
|
|
649
675
|
functional: { title: 'Funkcionalni', desc: 'Zapomnijo si vaše nastavitve, na primer jezik ali klepet, da jih ni treba določati znova.' },
|
|
@@ -672,6 +698,7 @@
|
|
|
672
698
|
colPurpose: 'Syfte',
|
|
673
699
|
colExpiry: 'Upphör',
|
|
674
700
|
floating: 'Cookieinställningar',
|
|
701
|
+
extraTitle: 'Ytterligare information',
|
|
675
702
|
cat: {
|
|
676
703
|
necessary: { title: 'Nödvändiga', desc: 'Krävs för att webbplatsen ska fungera — inloggning, säkerhet, minne av ditt samtycke. De kan inte stängas av.' },
|
|
677
704
|
functional: { title: 'Funktionella', desc: 'Kommer ihåg dina inställningar, till exempel språk eller chatt, så att du slipper välja igen.' },
|
|
@@ -700,6 +727,7 @@
|
|
|
700
727
|
colPurpose: 'Мета',
|
|
701
728
|
colExpiry: 'Термін',
|
|
702
729
|
floating: 'Налаштування cookie',
|
|
730
|
+
extraTitle: 'Додаткова інформація',
|
|
703
731
|
cat: {
|
|
704
732
|
necessary: { title: 'Необхідні', desc: 'Без них сайт не працює: вхід, безпека, пам’ять про ваш вибір. Вимкнути неможливо.' },
|
|
705
733
|
functional: { title: 'Функціональні', desc: 'Запам’ятовують ваші налаштування — наприклад мову або чат, — щоб ви не задавали їх знову.' },
|
|
@@ -728,6 +756,7 @@
|
|
|
728
756
|
colPurpose: 'Amaç',
|
|
729
757
|
colExpiry: 'Süre',
|
|
730
758
|
floating: 'Çerez ayarları',
|
|
759
|
+
extraTitle: 'Ek bilgiler',
|
|
731
760
|
cat: {
|
|
732
761
|
necessary: { title: 'Zorunlu', desc: 'Sitenin çalışması için gerekli: oturum açma, güvenlik, onayınızın hatırlanması. Kapatılamazlar.' },
|
|
733
762
|
functional: { title: 'İşlevsel', desc: 'Dil veya sohbet gibi tercihlerinizi hatırlar, böylece yeniden ayarlamanız gerekmez.' },
|
|
@@ -756,6 +785,7 @@
|
|
|
756
785
|
colPurpose: 'Formål',
|
|
757
786
|
colExpiry: 'Utløper',
|
|
758
787
|
floating: 'Innstillinger for informasjonskapsler',
|
|
788
|
+
extraTitle: 'Tilleggsinformasjon',
|
|
759
789
|
cat: {
|
|
760
790
|
necessary: { title: 'Nødvendige', desc: 'Kreves for at nettstedet skal virke — innlogging, sikkerhet, minne om samtykket ditt. De kan ikke slås av.' },
|
|
761
791
|
functional: { title: 'Funksjonelle', desc: 'Husker innstillingene dine, som språk eller chat, så du slipper å velge på nytt.' },
|
|
@@ -788,6 +818,7 @@
|
|
|
788
818
|
colPurpose: 'Tilgangur',
|
|
789
819
|
colExpiry: 'Gildistími',
|
|
790
820
|
floating: 'Stillingar vefkaka',
|
|
821
|
+
extraTitle: 'Viðbótarupplýsingar',
|
|
791
822
|
cat: {
|
|
792
823
|
necessary: { title: 'Nauðsynlegar', desc: 'Nauðsynlegar til að vefurinn virki — innskráning, öryggi, að muna samþykki þitt. Ekki er hægt að slökkva á þeim.' },
|
|
793
824
|
functional: { title: 'Virknivefkökur', desc: 'Muna stillingar þínar, til dæmis tungumál eða spjall, svo þú þurfir ekki að velja aftur.' },
|
|
@@ -817,6 +848,7 @@
|
|
|
817
848
|
colPurpose: 'Сврха',
|
|
818
849
|
colExpiry: 'Истиче',
|
|
819
850
|
floating: 'Подешавања колачића',
|
|
851
|
+
extraTitle: 'Додатне информације',
|
|
820
852
|
cat: {
|
|
821
853
|
necessary: { title: 'Неопходни', desc: 'Потребни су да би сајт радио — пријава, безбедност, памћење ваше сагласности. Не могу се искључити.' },
|
|
822
854
|
functional: { title: 'Функционални', desc: 'Памте ваша подешавања, на пример језик или ћаскање, да их не бисте бирали поново.' },
|
|
@@ -845,6 +877,7 @@
|
|
|
845
877
|
colPurpose: 'Finalitat',
|
|
846
878
|
colExpiry: 'Caduca',
|
|
847
879
|
floating: 'Configuració de galetes',
|
|
880
|
+
extraTitle: 'Informació addicional',
|
|
848
881
|
cat: {
|
|
849
882
|
necessary: { title: 'Necessàries', desc: 'Calen perquè el lloc funcioni: inici de sessió, seguretat i memòria del vostre consentiment. No es poden desactivar.' },
|
|
850
883
|
functional: { title: 'Funcionals', desc: 'Recorden les vostres preferències, com l’idioma o el xat, perquè no les hàgiu de tornar a configurar.' },
|
|
@@ -873,6 +906,7 @@
|
|
|
873
906
|
colPurpose: 'Qëllimi',
|
|
874
907
|
colExpiry: 'Skadon',
|
|
875
908
|
floating: 'Cilësimet e cookie-ve',
|
|
909
|
+
extraTitle: 'Informacione shtesë',
|
|
876
910
|
cat: {
|
|
877
911
|
necessary: { title: 'Të nevojshme', desc: 'Nevojiten që faqja të funksionojë — hyrja, siguria, ruajtja e pëlqimit tuaj. Nuk mund të fiken.' },
|
|
878
912
|
functional: { title: 'Funksionale', desc: 'Mbajnë mend preferencat tuaja, si gjuha ose biseda, që të mos i vendosni sërish.' },
|
|
@@ -901,6 +935,7 @@
|
|
|
901
935
|
colPurpose: 'Цел',
|
|
902
936
|
colExpiry: 'Истекува',
|
|
903
937
|
floating: 'Поставки за колачиња',
|
|
938
|
+
extraTitle: 'Дополнителни информации',
|
|
904
939
|
cat: {
|
|
905
940
|
necessary: { title: 'Неопходни', desc: 'Потребни се за страницата да работи — најава, безбедност, помнење на вашата согласност. Не можат да се исклучат.' },
|
|
906
941
|
functional: { title: 'Функционални', desc: 'Ги паметат вашите поставки, на пример јазикот или разговорот, за да не ги задавате повторно.' },
|
package/src/ck-ui.js
CHANGED
|
@@ -33,6 +33,15 @@
|
|
|
33
33
|
bannerLabel: 'Cookie consent',
|
|
34
34
|
panelTitle: 'Cookie settings',
|
|
35
35
|
panelIntro: 'Choose what to allow. By default only the necessary ones are on.',
|
|
36
|
+
/* SPEC V1.16 §1.2 — the «Additional information» card at the foot of the
|
|
37
|
+
panel: who the operator is, where to write, how long an answer takes,
|
|
38
|
+
where to complain. The HEADING has a real translation in every locale;
|
|
39
|
+
the BODY is deliberately empty in every dictionary, because the block
|
|
40
|
+
renders only when the site actually supplies text. A default sentence
|
|
41
|
+
here would put an empty operator card on every site that never
|
|
42
|
+
configured one. */
|
|
43
|
+
extraTitle: 'Additional information',
|
|
44
|
+
extraText: '',
|
|
36
45
|
save: 'Save choices',
|
|
37
46
|
close: 'Close',
|
|
38
47
|
alwaysOn: 'always on',
|
|
@@ -101,6 +110,10 @@
|
|
|
101
110
|
bannerLabel: 'Согласие на cookie',
|
|
102
111
|
panelTitle: 'Настройки cookie',
|
|
103
112
|
panelIntro: 'Выберите, что разрешить. По умолчанию включено только необходимое.',
|
|
113
|
+
// SPEC V1.16 §1.2. Заголовок переведён везде, тело пустое везде — см.
|
|
114
|
+
// комментарий у en выше.
|
|
115
|
+
extraTitle: 'Дополнительно',
|
|
116
|
+
extraText: '',
|
|
104
117
|
save: 'Сохранить выбор',
|
|
105
118
|
close: 'Закрыть',
|
|
106
119
|
alwaysOn: 'всегда активны',
|
|
@@ -169,7 +182,16 @@
|
|
|
169
182
|
// group's own one. Both are plain strings, so they belong here: a key left
|
|
170
183
|
// out of this list is `undefined` for all 32 external locales and renders
|
|
171
184
|
// the literal word "undefined" on the card.
|
|
172
|
-
'svcDetails', 'svcListLabel'
|
|
185
|
+
'svcDetails', 'svcListLabel',
|
|
186
|
+
/* SPEC V1.16 §1.2 — the «Дополнительно» card. Both belong here for the
|
|
187
|
+
usual reason (a DICT key missing from this list is `undefined` for all 32
|
|
188
|
+
external locales), but they behave differently on purpose:
|
|
189
|
+
|
|
190
|
+
extraTitle — translated everywhere, so every locale has a heading.
|
|
191
|
+
extraText — '' in every dictionary. The merge line below keeps '' when
|
|
192
|
+
the locale supplies nothing, and the panel treats '' as
|
|
193
|
+
«no block». Only config.texts.<lang>.extraText fills it. */
|
|
194
|
+
'extraTitle', 'extraText'
|
|
173
195
|
];
|
|
174
196
|
|
|
175
197
|
// Plural-form keys, filled separately from STR_KEYS (see above).
|
|
@@ -260,13 +282,74 @@
|
|
|
260
282
|
return list[idx].replace('{n}', String(n));
|
|
261
283
|
}
|
|
262
284
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
285
|
+
/* SPEC V1.16 §1.1 — per-language dictionary overrides from the CONFIG.
|
|
286
|
+
|
|
287
|
+
`config.texts` is a mixed bag: it has carried `policyUrl`, `detailsAction`,
|
|
288
|
+
`declarationUrl` and `cabinetUrl` since 0.5.0, and 0.5.15 adds `links`. So a
|
|
289
|
+
key is a language dictionary only when it LOOKS like a language tag — that
|
|
290
|
+
regex is the whole separation, and it is why no future scalar setting under
|
|
291
|
+
`texts` can ever be mistaken for one (none of them is two letters).
|
|
292
|
+
|
|
293
|
+
Own properties only, and the value must be a plain object: `texts` is
|
|
294
|
+
author-supplied JSON that may have been merged over a prototype, and
|
|
295
|
+
reading an inherited key would hand the merge below a function. */
|
|
296
|
+
var LANG_KEY_RE = /^[a-z]{2}(-[a-z]{2})?$/;
|
|
297
|
+
|
|
298
|
+
function configTexts(cfg, code) {
|
|
299
|
+
var texts = (cfg && cfg.texts && typeof cfg.texts === 'object' && !Array.isArray(cfg.texts))
|
|
300
|
+
? cfg.texts : null;
|
|
301
|
+
if (!texts) return null;
|
|
302
|
+
var k = String(code || '').toLowerCase();
|
|
303
|
+
if (!LANG_KEY_RE.test(k)) return null;
|
|
304
|
+
if (!Object.prototype.hasOwnProperty.call(texts, k)) return null;
|
|
305
|
+
var v = texts[k];
|
|
306
|
+
return (v && typeof v === 'object' && !Array.isArray(v)) ? v : null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* The keys an override may reach. A WHITELIST, not «whatever the object
|
|
310
|
+
carries»: an operator writing their own banner copy has no business
|
|
311
|
+
rewriting «Принять всё» (the button labels are what a visitor recognises
|
|
312
|
+
across sites) or the plural tables (an array here would break plural()).
|
|
313
|
+
`cat.<name>.title/desc` is handled separately below, one level deeper. */
|
|
314
|
+
var OVERRIDE_KEYS = [
|
|
315
|
+
'bannerTitle', 'bannerText', 'panelTitle', 'panelIntro', 'extraTitle', 'extraText'
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
// A non-empty string wins; empty or missing falls through to the layer below.
|
|
319
|
+
// That is what makes an empty field in the cabinet mean «take the dictionary»
|
|
320
|
+
// rather than «render nothing».
|
|
321
|
+
function pickOverride(src, key) {
|
|
322
|
+
if (!src) return null;
|
|
323
|
+
if (!Object.prototype.hasOwnProperty.call(src, key)) return null;
|
|
324
|
+
var v = src[key];
|
|
325
|
+
return (typeof v === 'string' && v) ? v : null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* Deep two-level fill from en: a partial locale must never yield undefined,
|
|
329
|
+
which would render the literal string "undefined".
|
|
330
|
+
|
|
331
|
+
Three layers as of 0.5.15: builtin DICT <- window.__ckLocales (both already
|
|
332
|
+
folded into `table`) <- config.texts[lang]. `cfg` is OPTIONAL on purpose —
|
|
333
|
+
placeholderText() and the cabinet's own calls ask for the plain dictionary,
|
|
334
|
+
and an absent third argument must build exactly what 0.5.14 built. */
|
|
335
|
+
function buildStrings(lang, table, cfg) {
|
|
266
336
|
var src = table[lang] || {};
|
|
267
337
|
var base = DICT.en;
|
|
268
338
|
var out = {};
|
|
269
339
|
var i, c;
|
|
340
|
+
|
|
341
|
+
/* Exact code first, then the two-letter base of the RESOLVED language:
|
|
342
|
+
a banner that resolved to `pt-br` takes `texts['pt-br']` over
|
|
343
|
+
`texts['pt']`, and a site that only wrote `texts.pt` still reaches it.
|
|
344
|
+
Same shape as resolveLang(), so the override follows the banner. */
|
|
345
|
+
var code = String(lang || '').toLowerCase();
|
|
346
|
+
var ov = configTexts(cfg, code);
|
|
347
|
+
var ovBase = (code.length > 2) ? configTexts(cfg, code.slice(0, 2)) : null;
|
|
348
|
+
|
|
349
|
+
function override(key) {
|
|
350
|
+
return pickOverride(ov, key) || pickOverride(ovBase, key);
|
|
351
|
+
}
|
|
352
|
+
|
|
270
353
|
for (i = 0; i < STR_KEYS.length; i++) {
|
|
271
354
|
var k = STR_KEYS[i];
|
|
272
355
|
out[k] = (typeof src[k] === 'string' && src[k]) ? src[k] : base[k];
|
|
@@ -290,9 +373,309 @@
|
|
|
290
373
|
desc: (typeof e.desc === 'string' && e.desc) ? e.desc : base.cat[c].desc
|
|
291
374
|
};
|
|
292
375
|
}
|
|
376
|
+
|
|
377
|
+
/* SPEC V1.16 §1.1 — the config layer, applied LAST so it wins over both the
|
|
378
|
+
builtin dictionary and __ckLocales. Only the whitelisted keys, and only
|
|
379
|
+
non-empty strings: an override that is absent, empty or not a string
|
|
380
|
+
leaves whatever the two lower layers produced.
|
|
381
|
+
|
|
382
|
+
Nothing here is ever treated as HTML. These strings go through el()'s
|
|
383
|
+
textContent and (for extraText) renderRich(), which builds nodes with
|
|
384
|
+
createElement/createTextNode — an author who writes «<b>» sees the
|
|
385
|
+
characters «<b>», which is the contract the server's validator mirrors. */
|
|
386
|
+
if (ov || ovBase) {
|
|
387
|
+
for (i = 0; i < OVERRIDE_KEYS.length; i++) {
|
|
388
|
+
var ok = OVERRIDE_KEYS[i];
|
|
389
|
+
var hit = override(ok);
|
|
390
|
+
if (hit) out[ok] = hit;
|
|
391
|
+
}
|
|
392
|
+
// cat.<name>.title / cat.<name>.desc, one level deeper. Read from a
|
|
393
|
+
// `cat` object on the override, with the same exact-then-base chain.
|
|
394
|
+
var ovCat = (ov && ov.cat && typeof ov.cat === 'object') ? ov.cat : null;
|
|
395
|
+
var ovBaseCat = (ovBase && ovBase.cat && typeof ovBase.cat === 'object') ? ovBase.cat : null;
|
|
396
|
+
for (i = 0; i < ALL_CATS.length; i++) {
|
|
397
|
+
c = ALL_CATS[i];
|
|
398
|
+
var oe = (ovCat && ovCat[c] && typeof ovCat[c] === 'object') ? ovCat[c] : null;
|
|
399
|
+
var obe = (ovBaseCat && ovBaseCat[c] && typeof ovBaseCat[c] === 'object') ? ovBaseCat[c] : null;
|
|
400
|
+
var t = pickOverride(oe, 'title') || pickOverride(obe, 'title');
|
|
401
|
+
var d = pickOverride(oe, 'desc') || pickOverride(obe, 'desc');
|
|
402
|
+
if (t) out.cat[c].title = t;
|
|
403
|
+
if (d) out.cat[c].desc = d;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* ------------------------------------------ rich text (SPEC V1.16 §1.2) */
|
|
410
|
+
|
|
411
|
+
/* The tiny markup subset the «Дополнительно» block accepts. It exists because
|
|
412
|
+
an operator block is genuinely structured text — two or three paragraphs, a
|
|
413
|
+
bold e-mail address, a link to the supervisory authority — and because the
|
|
414
|
+
alternative (accepting HTML) would mean sanitising author HTML in the
|
|
415
|
+
browser, which is a class of bug we are not going to own.
|
|
416
|
+
|
|
417
|
+
THE RULES, in the order they are applied. The server's validator mirrors
|
|
418
|
+
this exactly, so the order is part of the contract, not an implementation
|
|
419
|
+
detail:
|
|
420
|
+
|
|
421
|
+
1. A blank line (two newlines, any surrounding spaces) splits paragraphs.
|
|
422
|
+
Each paragraph becomes one <p>.
|
|
423
|
+
2. Inside a paragraph, a single newline becomes <br>.
|
|
424
|
+
3. LINKS ARE TOKENISED FIRST, before anything else looks at the text:
|
|
425
|
+
[label](URL) with URL http(s):// or mailto:
|
|
426
|
+
a bare https?:// URL
|
|
427
|
+
a bare e-mail address
|
|
428
|
+
Link-first is not a preference — it is what stops the bare-URL rule
|
|
429
|
+
from eating the address inside «[Центр](https://datepersonale.md)»
|
|
430
|
+
and stops the bare-e-mail rule from firing inside a mailto: label.
|
|
431
|
+
4. `**bold**` is applied only to the TEXT RUNS BETWEEN links. So bold does
|
|
432
|
+
not nest inside a link label and a link does not appear inside bold —
|
|
433
|
+
one pass, no nesting, nothing recursive to get wrong.
|
|
434
|
+
5. Any other URL scheme in [label](…) — javascript:, data:, file: — is
|
|
435
|
+
NOT a link. The WHOLE literal «[label](javascript:…)» is rendered as
|
|
436
|
+
plain text: visibly wrong to whoever wrote it, rather than silently
|
|
437
|
+
dropping the address and leaving a label that looks like a link.
|
|
438
|
+
6. There is no HTML. «<b>» is five characters of text, always.
|
|
439
|
+
|
|
440
|
+
Returns an ARRAY of nodes, so the caller decides where they go. Pure apart
|
|
441
|
+
from document.createElement, which is what makes «no innerHTML» mechanical
|
|
442
|
+
rather than a promise. */
|
|
443
|
+
|
|
444
|
+
// [label](url) — the label may not contain brackets, the url may not contain
|
|
445
|
+
// whitespace or a closing paren. Deliberately strict: an address that needs
|
|
446
|
+
// an escaped paren is an address that belongs in a plain bare-URL run.
|
|
447
|
+
var MD_LINK_RE = /\[([^\]\[]+)\]\(([^()\s]+)\)/;
|
|
448
|
+
var BARE_URL_RE = /https?:\/\/[^\s<>()\[\]"']+[^\s<>()\[\]"'.,;:!?]/;
|
|
449
|
+
// Simple on purpose: an address that this misses renders as plain text, which
|
|
450
|
+
// is a worse link, not a security hole. Anything clever here is a false
|
|
451
|
+
// positive waiting to turn a sentence into a mailto:.
|
|
452
|
+
var BARE_MAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+/;
|
|
453
|
+
|
|
454
|
+
// http(s) for an outbound link, mailto: for an address. Everything else is
|
|
455
|
+
// refused — a javascript: or data: URL in a control the visitor is invited to
|
|
456
|
+
// click is the XSS vector resolveDetails() already refuses for policyUrl.
|
|
457
|
+
function richHref(url) {
|
|
458
|
+
var u = str(url);
|
|
459
|
+
if (!u) return null;
|
|
460
|
+
if (/^https?:\/\//i.test(u)) return u;
|
|
461
|
+
if (/^mailto:[^\s]+@[^\s]+$/i.test(u)) return u;
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function richLink(label, href) {
|
|
466
|
+
var a = el('a', 'ck-rich__link', label);
|
|
467
|
+
a.href = href;
|
|
468
|
+
a.target = '_blank';
|
|
469
|
+
a.rel = 'noopener';
|
|
470
|
+
return a;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* `**bold**` over a plain run. Non-greedy and never across a newline, so an
|
|
474
|
+
unclosed «**» is two literal asterisks rather than a bold tail that
|
|
475
|
+
swallows the rest of the paragraph. */
|
|
476
|
+
var BOLD_RE = /\*\*([^*\n]+)\*\*/;
|
|
477
|
+
|
|
478
|
+
/* PASS 3 — bare addresses inside one run of plain text. Runs LAST, on text
|
|
479
|
+
that no longer contains any [label](url), so it cannot reach inside one. */
|
|
480
|
+
function richBare(text, out) {
|
|
481
|
+
var rest = String(text);
|
|
482
|
+
while (rest) {
|
|
483
|
+
var url = BARE_URL_RE.exec(rest);
|
|
484
|
+
var mail = BARE_MAIL_RE.exec(rest);
|
|
485
|
+
// Whichever address starts first. A URL wins a tie: «https://a@b.md» is
|
|
486
|
+
// an address with a userinfo part, not an e-mail sitting after a scheme.
|
|
487
|
+
var m = null, isMail = false;
|
|
488
|
+
if (url) { m = url; }
|
|
489
|
+
if (mail && (!m || mail.index < m.index)) { m = mail; isMail = true; }
|
|
490
|
+
if (!m) { out.push(document.createTextNode(rest)); return; }
|
|
491
|
+
if (m.index > 0) out.push(document.createTextNode(rest.slice(0, m.index)));
|
|
492
|
+
out.push(richLink(m[0], isMail ? ('mailto:' + m[0]) : m[0]));
|
|
493
|
+
rest = rest.slice(m.index + m[0].length);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/* PASS 2 — `**bold**` over one run of text, then PASS 3 inside each half.
|
|
498
|
+
|
|
499
|
+
Bold has to come BEFORE the bare-address rules, not after: «**mail@x.md**»
|
|
500
|
+
is one bold run containing an address, and a pass that linked the address
|
|
501
|
+
first would split the run in three and leave BOLD_RE looking at «**» and
|
|
502
|
+
«**» separately — the asterisks would survive on screen. That is the whole
|
|
503
|
+
reason these are three ordered passes rather than one left-to-right walk. */
|
|
504
|
+
function richBold(text, out) {
|
|
505
|
+
var rest = String(text);
|
|
506
|
+
var m;
|
|
507
|
+
while ((m = BOLD_RE.exec(rest))) {
|
|
508
|
+
if (m.index > 0) richBare(rest.slice(0, m.index), out);
|
|
509
|
+
var strong = el('strong', null);
|
|
510
|
+
var inner = [];
|
|
511
|
+
richBare(m[1], inner); // an address inside bold is still a link
|
|
512
|
+
for (var i = 0; i < inner.length; i++) strong.appendChild(inner[i]);
|
|
513
|
+
out.push(strong);
|
|
514
|
+
rest = rest.slice(m.index + m[0].length);
|
|
515
|
+
}
|
|
516
|
+
if (rest) richBare(rest, out);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/* PASS 1 — one line of a paragraph -> nodes.
|
|
520
|
+
|
|
521
|
+
`[label](url)` is tokenised first and its pieces never reach passes 2 and 3:
|
|
522
|
+
that is what stops the bare-URL rule from eating the address inside the
|
|
523
|
+
parens, stops the bare-e-mail rule from firing inside a mailto: label, and
|
|
524
|
+
leaves a label's own «**» literal (rule 4 of the contract). The text
|
|
525
|
+
BETWEEN markdown links goes through richBold(), which then runs richBare()
|
|
526
|
+
inside and outside each bold span. */
|
|
527
|
+
function richLine(line, out) {
|
|
528
|
+
var rest = String(line);
|
|
529
|
+
var m;
|
|
530
|
+
while ((m = MD_LINK_RE.exec(rest))) {
|
|
531
|
+
if (m.index > 0) richBold(rest.slice(0, m.index), out);
|
|
532
|
+
var href = richHref(m[2]);
|
|
533
|
+
// Rule 5: a refused scheme is the whole literal, verbatim and unbolded,
|
|
534
|
+
// so «[x](javascript:alert(1))» reads as exactly what was written.
|
|
535
|
+
if (href) out.push(richLink(m[1], href));
|
|
536
|
+
else out.push(document.createTextNode(m[0]));
|
|
537
|
+
rest = rest.slice(m.index + m[0].length);
|
|
538
|
+
}
|
|
539
|
+
if (rest) richBold(rest, out);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/* text -> array of <p> nodes. Empty (or non-string) input gives an empty
|
|
543
|
+
array, which is what lets the caller say «no text, no block» by asking for
|
|
544
|
+
the length rather than by re-testing the string. */
|
|
545
|
+
function renderRich(text) {
|
|
546
|
+
var s = (typeof text === 'string') ? text : '';
|
|
547
|
+
if (!s.trim()) return [];
|
|
548
|
+
// \r\n and \r normalised first: a value pasted from Windows or copied out of
|
|
549
|
+
// a textarea must split into the same paragraphs as one typed here.
|
|
550
|
+
var paras = s.replace(/\r\n?/g, '\n').split(/\n[ \t]*\n+/);
|
|
551
|
+
var out = [];
|
|
552
|
+
for (var i = 0; i < paras.length; i++) {
|
|
553
|
+
var para = paras[i].replace(/^\n+|\n+$/g, '');
|
|
554
|
+
if (!para.trim()) continue;
|
|
555
|
+
var p = el('p', 'ck-rich__p');
|
|
556
|
+
var lines = para.split('\n');
|
|
557
|
+
for (var j = 0; j < lines.length; j++) {
|
|
558
|
+
if (j > 0) p.appendChild(el('br')); // rule 2: a single newline
|
|
559
|
+
var nodes = [];
|
|
560
|
+
richLine(lines[j], nodes);
|
|
561
|
+
for (var k = 0; k < nodes.length; k++) p.appendChild(nodes[k]);
|
|
562
|
+
}
|
|
563
|
+
out.push(p);
|
|
564
|
+
}
|
|
565
|
+
return out;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/* ------------------------------------------ banner links (SPEC V1.16 §1.3) */
|
|
569
|
+
|
|
570
|
+
/* `texts.links` — up to three of the operator's own addresses, rendered under
|
|
571
|
+
the banner buttons and at the foot of the settings panel. The obvious pair
|
|
572
|
+
is «Политика конфиденциальности» and «Политика cookie», which a Moldovan
|
|
573
|
+
site is required to publish and which have nowhere else to go on the card.
|
|
574
|
+
|
|
575
|
+
Deliberately NOT tangled with `detailsAction`: «Подробнее» keeps doing
|
|
576
|
+
exactly what it did in 0.5.14, because a site that gains a link row must
|
|
577
|
+
not silently lose the link it already had.
|
|
578
|
+
|
|
579
|
+
A row is skipped, never faked, when its address is not http(s) or when no
|
|
580
|
+
label resolves for the banner's language — a link with no words on it is
|
|
581
|
+
not a link. The cap is applied to the SURVIVORS: one malformed row must not
|
|
582
|
+
cost a good one its place. */
|
|
583
|
+
function linkLabel(labels, lang) {
|
|
584
|
+
if (!labels || typeof labels !== 'object' || Array.isArray(labels)) return null;
|
|
585
|
+
var code = (typeof lang === 'string') ? lang.toLowerCase() : '';
|
|
586
|
+
// Own properties only, exactly as branding's pickPoweredByText does it and
|
|
587
|
+
// for the same reason: `labels` is author-supplied JSON, and an inherited
|
|
588
|
+
// key would hand str() a function instead of a missing line.
|
|
589
|
+
function own(k) {
|
|
590
|
+
return (k && Object.prototype.hasOwnProperty.call(labels, k)) ? str(labels[k]) : null;
|
|
591
|
+
}
|
|
592
|
+
var hit = own(code);
|
|
593
|
+
if (!hit && code.length > 2) hit = own(code.slice(0, 2)); // ru-RU -> ru
|
|
594
|
+
return hit || own('en');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
var MAX_LINKS = 3;
|
|
598
|
+
|
|
599
|
+
function resolveLinks(cfg, lang) {
|
|
600
|
+
var texts = (cfg && cfg.texts && typeof cfg.texts === 'object') ? cfg.texts : null;
|
|
601
|
+
var list = texts && texts.links;
|
|
602
|
+
if (!list || !Array.isArray(list)) return [];
|
|
603
|
+
var out = [];
|
|
604
|
+
for (var i = 0; i < list.length && out.length < MAX_LINKS; i++) {
|
|
605
|
+
var row = list[i];
|
|
606
|
+
if (!row || typeof row !== 'object') continue;
|
|
607
|
+
var url = str(row.url);
|
|
608
|
+
if (!url || !/^https?:\/\//i.test(url)) continue;
|
|
609
|
+
var label = linkLabel(row.label, lang);
|
|
610
|
+
if (!label) continue;
|
|
611
|
+
out.push({ id: str(row.id) || ('link' + i), url: url, label: label });
|
|
612
|
+
}
|
|
293
613
|
return out;
|
|
294
614
|
}
|
|
295
615
|
|
|
616
|
+
/* SPEC V1.16 §1.3, last sentence — the one place where the link row DOES
|
|
617
|
+
touch «Подробнее»: when the address it would send the visitor to is
|
|
618
|
+
already one of the rows under the buttons, the banner shows the same
|
|
619
|
+
address twice. The second copy is not more information, it is noise, so
|
|
620
|
+
the in-text link is dropped and the banner renders as `detailsAction:
|
|
621
|
+
'hide'` for that render — the link row keeps the address, with a label the
|
|
622
|
+
operator wrote, which is the better of the two.
|
|
623
|
+
|
|
624
|
+
Only 'policy' and 'declaration' can duplicate anything: 'settings' opens
|
|
625
|
+
the panel and has no address to collide with, and 'hide' is already gone.
|
|
626
|
+
|
|
627
|
+
Comparison is deliberately narrow. Trimming and a trailing slash are
|
|
628
|
+
typography, not identity — `https://shop.md/cookies` and
|
|
629
|
+
`…/cookies/` are the same page — and so is the case of the HOST, which
|
|
630
|
+
DNS does not distinguish. The PATH is left alone: `/Cookies` and
|
|
631
|
+
`/cookies` are genuinely two addresses on a case-sensitive server, and
|
|
632
|
+
suppressing a real «Подробнее» link over that guess is the worse error. */
|
|
633
|
+
function normHref(v) {
|
|
634
|
+
var s = str(v);
|
|
635
|
+
if (!s) return null;
|
|
636
|
+
var m = /^(https?:\/\/[^\/?#]*)([\s\S]*)$/i.exec(s);
|
|
637
|
+
if (!m) return null;
|
|
638
|
+
return (m[1].toLowerCase() + m[2]).replace(/\/$/, '');
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function detailsDuplicatesLink(details, links) {
|
|
642
|
+
if (!details) return false;
|
|
643
|
+
if (details.kind !== 'policy' && details.kind !== 'declaration') return false;
|
|
644
|
+
var href = normHref(details.href);
|
|
645
|
+
if (!href || !links || !Array.isArray(links)) return false;
|
|
646
|
+
for (var i = 0; i < links.length; i++) {
|
|
647
|
+
if (links[i] && normHref(links[i].url) === href) return true;
|
|
648
|
+
}
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/* Which of the three shapes the banner will actually draw, duplicate rule
|
|
653
|
+
applied — so buildBanner() and signature() cannot disagree about it.
|
|
654
|
+
`lang` is the code the banner resolved to; signature() passes the config's
|
|
655
|
+
own so both sides of a comparison are read in the same language. */
|
|
656
|
+
function detailsKind(cfg, lang) {
|
|
657
|
+
var det = resolveDetails(cfg);
|
|
658
|
+
var code = (typeof lang === 'string' && lang) ? lang :
|
|
659
|
+
resolveLang(cfg && cfg.language, localeTable());
|
|
660
|
+
return detailsDuplicatesLink(det, resolveLinks(cfg, code)) ? 'hide' : det.kind;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// The row itself, or null when there is nothing to draw — so both call sites
|
|
664
|
+
// (banner and panel) are one `if` rather than two.
|
|
665
|
+
function buildLinksRow(cfg, cls) {
|
|
666
|
+
var links = resolveLinks(cfg, LANG);
|
|
667
|
+
if (!links.length) return null;
|
|
668
|
+
var row = el('div', 'ck-links' + (cls ? ' ' + cls : ''));
|
|
669
|
+
for (var i = 0; i < links.length; i++) {
|
|
670
|
+
var a = el('a', 'ck-links__a', links[i].label);
|
|
671
|
+
a.href = links[i].url;
|
|
672
|
+
a.target = '_blank';
|
|
673
|
+
a.rel = 'noopener';
|
|
674
|
+
row.appendChild(a);
|
|
675
|
+
}
|
|
676
|
+
return row;
|
|
677
|
+
}
|
|
678
|
+
|
|
296
679
|
/* ------------------------------------------- blocked-embed placeholder (§2) */
|
|
297
680
|
|
|
298
681
|
/* The one piece of the placeholder that is worth testing without a DOM: which
|
|
@@ -537,6 +920,40 @@
|
|
|
537
920
|
>= 4.5:1 rule as «Подробнее» — see the note on the `a{}` rule above. */
|
|
538
921
|
'.ck-svc__policy{display:inline-block;margin-top:4px;font-size:12.5px;color:var(--ck-link)}',
|
|
539
922
|
|
|
923
|
+
/* ---- «Дополнительно» card (SPEC V1.16 §1.2) ---- */
|
|
924
|
+
/* A card, not another category row: it carries no switch and answers a
|
|
925
|
+
different question — who the operator is and how to reach them. So it
|
|
926
|
+
gets the soft fill and the rounded border the cookie table wears, which
|
|
927
|
+
reads as «a panel of reference text» rather than «a fifth thing you can
|
|
928
|
+
turn off».
|
|
929
|
+
|
|
930
|
+
It is appended AFTER the categories, which means the last .ck-cat is no
|
|
931
|
+
longer `:last-child` and keeps its bottom rule — that is wanted here: the
|
|
932
|
+
rule is what separates the last category from this block. The rule below
|
|
933
|
+
restores the old look for the category above it only in the sense that
|
|
934
|
+
the separator now belongs to the boundary rather than to the group. */
|
|
935
|
+
'.ck-extra{margin:16px 0 4px;padding:14px 16px;background:var(--ck-soft);',
|
|
936
|
+
'border:1px solid var(--ck-line);border-radius:var(--ck-radius-card)}',
|
|
937
|
+
'.ck-extra__title{margin:0 0 6px;font-size:15px;font-weight:600}',
|
|
938
|
+
/* Same size and colour as .ck-cat__desc: this is secondary reference text,
|
|
939
|
+
and it is measured for AA against the card by the same rule. */
|
|
940
|
+
'.ck-extra__body p{margin:0 0 8px;font-size:13.5px;color:var(--ck-muted);line-height:1.5}',
|
|
941
|
+
'.ck-extra__body p:last-child{margin-bottom:0}',
|
|
942
|
+
/* --ck-link, not --ck-accent — accent-coloured TEXT answers to 4.5:1
|
|
943
|
+
against the card; see the note on the `a{}` rule above. */
|
|
944
|
+
'.ck-extra__body a{color:var(--ck-link)}',
|
|
945
|
+
|
|
946
|
+
/* ---- link row under the buttons (SPEC V1.16 §1.3) ---- */
|
|
947
|
+
/* One row, wrapping. `width:100%` matters in the bar layout, whose banner is
|
|
948
|
+
a flex row: without it the row would try to sit BESIDE the buttons on a
|
|
949
|
+
wide screen instead of under them. */
|
|
950
|
+
'.ck-links{display:flex;gap:14px;flex-wrap:wrap;width:100%;margin-top:12px;font-size:13px}',
|
|
951
|
+
'.ck-links__a{color:var(--ck-link)}',
|
|
952
|
+
/* The panel foot is a flex row of buttons that each take `flex:1 1 150px`;
|
|
953
|
+
a full-width row forced onto its own line keeps the links under the
|
|
954
|
+
buttons rather than wedged between two of them. */
|
|
955
|
+
'.ck-links--panel{flex-basis:100%;margin-top:2px}',
|
|
956
|
+
|
|
540
957
|
/* ---- switch ---- */
|
|
541
958
|
'.ck-switch{flex:none;width:46px;height:27px;padding:0;border-radius:999px;',
|
|
542
959
|
'border:1px solid var(--ck-line);background:var(--ck-soft);position:relative}',
|
|
@@ -585,6 +1002,9 @@
|
|
|
585
1002
|
'.ck-banner--bar .ck-banner__body{display:contents}',
|
|
586
1003
|
'.ck-banner--bar .ck-banner__body>*{order:1}',
|
|
587
1004
|
'.ck-banner--bar .ck-actions{order:2}',
|
|
1005
|
+
// SPEC V1.16 §1.3 — the link row is `order:3` so it stays under the buttons
|
|
1006
|
+
// in the bar's mobile column, where every child's position is explicit.
|
|
1007
|
+
'.ck-banner--bar .ck-links{order:3}',
|
|
588
1008
|
/* Reference: the box's button row becomes a column under 560px. */
|
|
589
1009
|
'.ck-banner--box .ck-actions{flex-direction:column}',
|
|
590
1010
|
'.ck-banner--box .ck-btn{flex:1 1 auto;width:100%}}',
|
|
@@ -2088,6 +2508,14 @@
|
|
|
2088
2508
|
|
|
2089
2509
|
var p = el('p');
|
|
2090
2510
|
var det = resolveDetails(cfg);
|
|
2511
|
+
/* SPEC V1.16 §1.3 — «Подробнее» and one of the link rows point at the same
|
|
2512
|
+
page: drop the in-text copy rather than print the address twice. Asked
|
|
2513
|
+
through detailsKind() so the shape drawn here is the same one signature()
|
|
2514
|
+
watches for changes. The links are resolved for THIS render's language,
|
|
2515
|
+
exactly as buildLinksRow does below, so a row that no visitor of this
|
|
2516
|
+
banner can see (no label in their language) never silences the link
|
|
2517
|
+
they can. */
|
|
2518
|
+
if (detailsKind(cfg, LANG) === 'hide') det = { kind: 'hide', href: null };
|
|
2091
2519
|
if (det.kind === 'hide') {
|
|
2092
2520
|
p.appendChild(document.createTextNode(T.bannerText));
|
|
2093
2521
|
} else {
|
|
@@ -2148,6 +2576,18 @@
|
|
|
2148
2576
|
actions.appendChild(custom);
|
|
2149
2577
|
b.appendChild(actions);
|
|
2150
2578
|
|
|
2579
|
+
/* SPEC V1.16 §1.3 — the link row, under the buttons in all three layouts.
|
|
2580
|
+
Appended to the BANNER rather than to the body, so it sits after the
|
|
2581
|
+
actions in DOM (and reading) order everywhere: box and modal stack, and
|
|
2582
|
+
the bar's `width:100%` on .ck-links forces it onto its own line under
|
|
2583
|
+
the row instead of squeezing in beside the buttons.
|
|
2584
|
+
|
|
2585
|
+
The bar's mobile media query re-orders its children explicitly (body
|
|
2586
|
+
contents `order:1`, actions `order:2`); the row carries `order:3` there
|
|
2587
|
+
so it stays below the buttons rather than jumping above them. */
|
|
2588
|
+
var blinks = buildLinksRow(cfg, 'ck-links--banner');
|
|
2589
|
+
if (blinks) b.appendChild(blinks);
|
|
2590
|
+
|
|
2151
2591
|
// box/modal: the foot follows the buttons (see the note above).
|
|
2152
2592
|
if (foot && lay.type !== 'bar') b.appendChild(foot);
|
|
2153
2593
|
|
|
@@ -2159,6 +2599,21 @@
|
|
|
2159
2599
|
root.appendChild(b);
|
|
2160
2600
|
}
|
|
2161
2601
|
|
|
2602
|
+
/* SPEC V1.16 §1.2 — the «Дополнительно» card, or null when the site supplied
|
|
2603
|
+
no text for this language. T.extraText has already been through the whole
|
|
2604
|
+
merge (dictionary '' <- __ckLocales <- config.texts[lang] <- [base]), so
|
|
2605
|
+
the only question left here is whether anything survived it. */
|
|
2606
|
+
function buildExtra() {
|
|
2607
|
+
var nodes = renderRich(T.extraText);
|
|
2608
|
+
if (!nodes.length) return null;
|
|
2609
|
+
var box = el('div', 'ck-extra');
|
|
2610
|
+
box.appendChild(el('h3', 'ck-extra__title', T.extraTitle));
|
|
2611
|
+
var bodyEl = el('div', 'ck-extra__body');
|
|
2612
|
+
for (var i = 0; i < nodes.length; i++) bodyEl.appendChild(nodes[i]);
|
|
2613
|
+
box.appendChild(bodyEl);
|
|
2614
|
+
return box;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2162
2617
|
function buildPanel(cfg) {
|
|
2163
2618
|
var scrim = el('div', 'ck-panel-scrim ck-hidden');
|
|
2164
2619
|
scrim.setAttribute('aria-hidden', 'true');
|
|
@@ -2186,6 +2641,18 @@
|
|
|
2186
2641
|
var body = el('div', 'ck-panel__body');
|
|
2187
2642
|
var cats = ['necessary'].concat(activeOptIn(cfg));
|
|
2188
2643
|
for (var i = 0; i < cats.length; i++) body.appendChild(buildCategory(cfg, cats[i]));
|
|
2644
|
+
|
|
2645
|
+
/* SPEC V1.16 §1.2 — «Дополнительно», after the categories and their service
|
|
2646
|
+
lists, before the buttons. INSIDE .ck-panel__body, which is the scrolling
|
|
2647
|
+
region: an operator block runs to a paragraph or three, and put between
|
|
2648
|
+
the body and the foot it would be clipped on a short viewport with no way
|
|
2649
|
+
to reach the end of it.
|
|
2650
|
+
|
|
2651
|
+
Rendered only when the resolved extraText is a non-empty string — every
|
|
2652
|
+
dictionary ships '', so a site that never configured one draws nothing
|
|
2653
|
+
and the panel is byte-for-byte what 0.5.14 drew. */
|
|
2654
|
+
var extra = buildExtra();
|
|
2655
|
+
if (extra) body.appendChild(extra);
|
|
2189
2656
|
p.appendChild(body);
|
|
2190
2657
|
|
|
2191
2658
|
var foot = el('div', 'ck-panel__foot');
|
|
@@ -2213,6 +2680,11 @@
|
|
|
2213
2680
|
foot.appendChild(save);
|
|
2214
2681
|
foot.appendChild(acc);
|
|
2215
2682
|
foot.appendChild(rej);
|
|
2683
|
+
// SPEC V1.16 §1.3 — the operator's own addresses, under the panel's buttons
|
|
2684
|
+
// and above the attribution, so the bottom of the panel reads: what you can
|
|
2685
|
+
// do, then where to read more, then who made this.
|
|
2686
|
+
var plinks = buildLinksRow(cfg, 'ck-links--panel');
|
|
2687
|
+
if (plinks) foot.appendChild(plinks);
|
|
2216
2688
|
// Same attribution foot as the banner: mark and credit sign the bottom,
|
|
2217
2689
|
// below the action buttons, never the panel heading.
|
|
2218
2690
|
var pfoot = buildPoweredBy(cfg);
|
|
@@ -2769,7 +3241,14 @@
|
|
|
2769
3241
|
// Structural: link / button / nothing are three different DOM shapes,
|
|
2770
3242
|
// and mount() is one-shot. Buttons and colours are deliberately NOT
|
|
2771
3243
|
// here — they are token values and restyle in place.
|
|
2772
|
-
|
|
3244
|
+
/* SPEC V1.16 §1.3 — the EFFECTIVE kind, not the configured one: a config
|
|
3245
|
+
whose policyUrl grows into (or out of) one of its own link rows flips
|
|
3246
|
+
the shape between link and nothing while resolveDetails still answers
|
|
3247
|
+
'policy'. Without the fold, a SaaS config arriving after the first
|
|
3248
|
+
mount would leave the duplicate on screen. The language is the config's
|
|
3249
|
+
own, resolved the way mount() resolves it, so the two sides of a
|
|
3250
|
+
comparison are always read in the same language. */
|
|
3251
|
+
detailsKind(c),
|
|
2773
3252
|
/* SPEC V1.12 §3 — services are STRUCTURAL: each one adds a row with its
|
|
2774
3253
|
own switch to the panel, and mount() is one-shot. Without this a SaaS
|
|
2775
3254
|
config that arrives after the first mount (the second, idempotent
|
|
@@ -2782,10 +3261,43 @@
|
|
|
2782
3261
|
`purpose` is text inside an existing row and does not justify tearing
|
|
2783
3262
|
the panel down. */
|
|
2784
3263
|
serviceSignature(c),
|
|
3264
|
+
/* SPEC V1.16 §1 — custom texts are STRUCTURAL for the same reason
|
|
3265
|
+
services are: the «Дополнительно» card and the link row are DOM nodes
|
|
3266
|
+
that appear and disappear, and mount() is one-shot. A SaaS config that
|
|
3267
|
+
arrives after the first render (the second, idempotent init()) would
|
|
3268
|
+
otherwise leave a panel with no operator block on a site that has one.
|
|
3269
|
+
|
|
3270
|
+
Not the texts themselves, which can be long: the LANGUAGE KEYS that
|
|
3271
|
+
carry overrides, whether an extra block would render, and the links'
|
|
3272
|
+
identity. A reworded sentence inside a block that is already on screen
|
|
3273
|
+
does not justify tearing the panel down mid-decision. */
|
|
3274
|
+
textsSignature(c),
|
|
2785
3275
|
brandSignature(c)
|
|
2786
3276
|
].join('|');
|
|
2787
3277
|
}
|
|
2788
3278
|
|
|
3279
|
+
function textsSignature(cfg) {
|
|
3280
|
+
var texts = (cfg && cfg.texts && typeof cfg.texts === 'object' && !Array.isArray(cfg.texts))
|
|
3281
|
+
? cfg.texts : null;
|
|
3282
|
+
if (!texts) return '0';
|
|
3283
|
+
var langs = [];
|
|
3284
|
+
for (var k in texts) {
|
|
3285
|
+
if (!Object.prototype.hasOwnProperty.call(texts, k)) continue;
|
|
3286
|
+
if (LANG_KEY_RE.test(String(k).toLowerCase())) langs.push(String(k).toLowerCase());
|
|
3287
|
+
}
|
|
3288
|
+
langs.sort();
|
|
3289
|
+
var list = texts.links;
|
|
3290
|
+
var ids = [];
|
|
3291
|
+
if (Array.isArray(list)) {
|
|
3292
|
+
for (var i = 0; i < list.length; i++) {
|
|
3293
|
+
var row = list[i];
|
|
3294
|
+
if (row && typeof row === 'object') ids.push(String(row.id || i) + ':' + String(row.url || ''));
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
if (!langs.length && !ids.length) return '0';
|
|
3298
|
+
return langs.join(',') + '/' + ids.join(',');
|
|
3299
|
+
}
|
|
3300
|
+
|
|
2789
3301
|
function serviceSignature(cfg) {
|
|
2790
3302
|
var list = cfg && cfg.services;
|
|
2791
3303
|
if (!list || !Array.isArray(list) || !list.length) return '0';
|
|
@@ -2828,7 +3340,10 @@
|
|
|
2828
3340
|
|
|
2829
3341
|
var table = localeTable(); // read at render time
|
|
2830
3342
|
LANG = resolveLang(cfg && cfg.language, table);
|
|
2831
|
-
|
|
3343
|
+
// SPEC V1.16 §1.1 — the config is the third merge layer, so it must be
|
|
3344
|
+
// handed in here: this is the only place that knows which language the
|
|
3345
|
+
// banner actually resolved to, which is what the override is keyed by.
|
|
3346
|
+
T = buildStrings(LANG, table, cfg);
|
|
2832
3347
|
|
|
2833
3348
|
host = document.getElementById('ck-root');
|
|
2834
3349
|
if (!host) {
|
|
@@ -2964,6 +3479,22 @@
|
|
|
2964
3479
|
servicePurpose: servicePurpose,
|
|
2965
3480
|
groupCountLabel: groupCountLabel,
|
|
2966
3481
|
serviceSignature: serviceSignature,
|
|
3482
|
+
/* SPEC V1.16 §1 — the pure halves of the custom-text feature, so the
|
|
3483
|
+
cabinet's live preview quotes the same rules the banner paints and the
|
|
3484
|
+
server's validator can be tested against the same contract.
|
|
3485
|
+
|
|
3486
|
+
renderRich() is the exception to «nothing here touches the DOM»: it
|
|
3487
|
+
BUILDS nodes, so it needs document.createElement. That is deliberate —
|
|
3488
|
+
the whole point of the function is that there is no innerHTML anywhere
|
|
3489
|
+
in it, and a version that returned a string could not promise that. It
|
|
3490
|
+
is still pure in every other sense: same input, same nodes, no reads of
|
|
3491
|
+
module state. In a context with no `document` it is simply not called. */
|
|
3492
|
+
renderRich: renderRich,
|
|
3493
|
+
resolveLinks: resolveLinks,
|
|
3494
|
+
linkLabel: linkLabel,
|
|
3495
|
+
detailsDuplicatesLink: detailsDuplicatesLink,
|
|
3496
|
+
detailsKind: detailsKind,
|
|
3497
|
+
textsSignature: textsSignature,
|
|
2967
3498
|
signature: signature
|
|
2968
3499
|
};
|
|
2969
3500
|
// The page-font probe reads the DOM, so it is not part of the pure block —
|