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