Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/cookie-consent/translations.ts
14422 views
1
/*
2
* This file is part of CoCalc: Copyright © 2026 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import type { Translation } from "vanilla-cookieconsent";
7
8
import { COOKIE_CATEGORIES } from "./categories";
9
10
// English-only for the first version. The rest of CoCalc uses simplelocalize +
11
// JSON files; integrating the cookie banner with that pipeline is deferred to
12
// a follow-up PR. The vanilla-cookieconsent `autoDetect: 'browser'` setting
13
// still works — every locale just falls back to the `en` translation here.
14
15
export function buildTranslation(
16
descHtml: string,
17
privacyUrl: string,
18
termsUrl: string,
19
): Translation {
20
const footerLinks = `<a href="${privacyUrl}" target="_blank" rel="noopener noreferrer">Privacy policy</a>\n<a href="${termsUrl}" target="_blank" rel="noopener noreferrer">Terms of service</a>`;
21
// The preferences modal has no built-in footer slot in v3, so we append the
22
// policy links to the lead-in description as a small paragraph.
23
const prefsLead = `${descHtml}\n<p style="margin-top: 0.75em; font-size: 0.9em;">${footerLinks.replace("\n", " · ")}</p>`;
24
// Per-category sections derive from COOKIE_CATEGORIES, so adding a new
25
// category there automatically adds it to the preferences modal too.
26
const categorySections = COOKIE_CATEGORIES.map((c) => ({
27
title: c.label,
28
description: c.description,
29
linkedCategory: c.key,
30
}));
31
// Embedded YouTube videos use a separate consent flag (see
32
// cookie-consent/youtube.ts) so that accepting a video does not mark the
33
// main banner as decided. We still surface it in the preferences modal
34
// so users can review/revoke it alongside the v3 categories. The button
35
// is wired up by init.ts on `cc:onModalShow`.
36
const youtubeSection = {
37
title: "Embedded videos",
38
description: buildYouTubeSectionHtml(),
39
};
40
return {
41
consentModal: {
42
title: "We value your privacy",
43
description: descHtml,
44
acceptAllBtn: "Accept all",
45
acceptNecessaryBtn: "Necessary only",
46
showPreferencesBtn: "Manage preferences",
47
footer: footerLinks,
48
},
49
preferencesModal: {
50
title: "Cookie preferences",
51
acceptAllBtn: "Accept all",
52
acceptNecessaryBtn: "Necessary only",
53
savePreferencesBtn: "Save preferences",
54
closeIconLabel: "Close",
55
sections: [
56
{ description: prefsLead },
57
...categorySections,
58
youtubeSection,
59
],
60
},
61
};
62
}
63
64
// Container HTML for the "Embedded videos" preferences section. The
65
// toggle state and status badge are filled in at modal-open time by
66
// init.ts so they reflect the current cookie state without our having to
67
// re-render the v3 modal config.
68
//
69
// The styling here mimics v3's own per-category section so the YouTube
70
// row visually slots in alongside Necessary / Analytics / Usage even
71
// though it isn't backed by a real v3 category. Scoped class names
72
// (`cocalc-yt-*`) avoid colliding with v3's `pm__` namespace.
73
export const YOUTUBE_SECTION_STATUS_ID = "cocalc-yt-status";
74
export const YOUTUBE_SECTION_TOGGLE_ID = "cocalc-yt-toggle";
75
// Kept as an alias so init.ts doesn't have to know which DOM element it
76
// is actually toggling (we switched from a <button> to a checkbox).
77
export const YOUTUBE_SECTION_BUTTON_ID = YOUTUBE_SECTION_TOGGLE_ID;
78
79
// CSS for the YouTube section. Three constraints conspire here:
80
//
81
// 1. v3 injects `section.description` via innerHTML into a <p>, so block
82
// children (<div>, <style>) get ejected by the HTML parser. We use
83
// only inline elements (<span>/<label>) below.
84
// 2. v3's stylesheet has a top-of-file rule
85
// `#cc-main :before, #cc-main span, #cc-main input ... { all: unset }`
86
// which carries the specificity of an id selector. Plain class
87
// selectors lose to it, so every rule below is scoped under
88
// `#cc-main` to match and source-order-override the reset.
89
// 3. The stylesheet is mounted into <head> by init.ts so it works
90
// regardless of where in the cookie-consent modal the markup ends up.
91
export const YOUTUBE_SECTION_CSS = `
92
#cc-main .cocalc-yt-card {
93
display: inline-flex;
94
align-items: center;
95
gap: 1em;
96
width: 100%;
97
box-sizing: border-box;
98
margin-top: 0.75em;
99
padding: 0.75em 1em;
100
border: 1px solid var(--cc-toggle-border-color, #d1d5db);
101
border-radius: 0.5em;
102
background: var(--cc-section-category-block-bg, #f9fafb);
103
vertical-align: top;
104
}
105
#cc-main .cocalc-yt-card__text {
106
flex: 1 1 auto;
107
min-width: 0;
108
display: inline-flex;
109
flex-direction: column;
110
gap: 0.25em;
111
align-items: flex-start;
112
}
113
#cc-main .cocalc-yt-card__label {
114
font-weight: 600;
115
display: inline-block;
116
}
117
#cc-main .cocalc-yt-status {
118
display: inline-block;
119
padding: 0.15em 0.6em;
120
border-radius: 999px;
121
font-size: 0.85em;
122
font-weight: 600;
123
line-height: 1.4;
124
}
125
#cc-main .cocalc-yt-status--on {
126
background: #d1fae5;
127
color: #065f46;
128
}
129
#cc-main .cocalc-yt-status--off {
130
background: #fee2e2;
131
color: #991b1b;
132
}
133
#cc-main .cocalc-yt-switch {
134
position: relative;
135
display: inline-block;
136
width: 44px;
137
height: 24px;
138
flex: 0 0 auto;
139
cursor: pointer;
140
}
141
#cc-main .cocalc-yt-switch input {
142
position: absolute;
143
opacity: 0;
144
width: 0;
145
height: 0;
146
}
147
#cc-main .cocalc-yt-slider {
148
position: absolute;
149
inset: 0;
150
background: #cbd5e1;
151
border-radius: 999px;
152
transition: background 0.2s;
153
display: inline-block;
154
}
155
#cc-main .cocalc-yt-slider::before {
156
content: "";
157
position: absolute;
158
width: 18px;
159
height: 18px;
160
left: 3px;
161
top: 3px;
162
background: white;
163
border-radius: 50%;
164
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
165
transition: transform 0.2s;
166
}
167
#cc-main .cocalc-yt-switch input:checked + .cocalc-yt-slider {
168
background: #10b981;
169
}
170
#cc-main .cocalc-yt-switch input:checked + .cocalc-yt-slider::before {
171
transform: translateX(20px);
172
}
173
#cc-main .cocalc-yt-switch input:focus-visible + .cocalc-yt-slider {
174
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.35);
175
}
176
`;
177
178
export function buildYouTubeSectionHtml(): string {
179
// Every wrapper is an inline element so the surrounding <p> v3 creates
180
// for `section.description` stays valid HTML. Visual block layout is
181
// recovered via display:inline-flex / inline-block in YOUTUBE_SECTION_CSS.
182
return `
183
<span>
184
Some pages embed YouTube videos. Playing them allows YouTube to set
185
cookies in your browser, separately from the cookies described above.
186
Videos stay blocked until you click them.
187
</span>
188
<span class="cocalc-yt-card">
189
<span class="cocalc-yt-card__text">
190
<span class="cocalc-yt-card__label">Embedded YouTube videos</span>
191
<span id="${YOUTUBE_SECTION_STATUS_ID}" class="cocalc-yt-status cocalc-yt-status--off">Blocked</span>
192
</span>
193
<label class="cocalc-yt-switch" aria-label="Allow embedded YouTube videos">
194
<input id="${YOUTUBE_SECTION_TOGGLE_ID}" type="checkbox" />
195
<span class="cocalc-yt-slider"></span>
196
</label>
197
</span>`;
198
}
199
200