fix(gallery): keep canvas rendering in the lightbox, render tiles as <img>

Every tile, the hero and the folder covers switched to a <canvas> when
the per-event toggle was on or the protection level was `maximum`. A
canvas pins a backing store of naturalWidth × naturalHeight × 4 bytes
that the browser is not allowed to evict, and iOS Safari has a hard
budget for canvas memory that fails silently when exceeded — blank
tiles, no error, on exactly the browser the large-gallery report came
from. A gallery is several hundred tiles and one lightbox image.

What canvas buys on a thumbnail is a slightly harder right-click. What
actually protects the images is server-side: the served file is
watermarked and the download route refuses when downloads are off. The
photographer who reported the large-gallery case, shipping to real
clients, said the same and turned the global toggle off once it was
about to reach their next gallery.

So: tiles, hero and folder covers always render <img>. The lightbox
keeps both the per-event toggle and the `maximum` implication — one
image, where the calculus is different. The toggle is now wired to the
lightbox for the first time; before this it reached only the tiles, so
the label that said "canvas rendering" turned every grid into canvases
and left the lightbox alone. Labels in all four locales now say where it
applies.

`protectionLevel` was destructured in seven tile components only to feed
that OR; those props and their pass-throughs go with it. The shared
layout props keep it, since the story layout still hands it to its
lightbox.

A source-level test pins that only PhotoLightbox passes
useCanvasRendering to AuthenticatedImage or turns it on for `maximum`.

Relates to issue 1287
This commit is contained in:
Paul Nothaft
2026-09-06 21:13:37 +02:00
parent c4b03a831f
commit c75839d3ea
20 changed files with 68 additions and 81 deletions
@@ -22,9 +22,7 @@ interface GalleryFolderTilesProps {
* so a gallery configured for canvas rendering or maximum protection must not
* get an ordinary blob-backed <img> here just because it is a cover.
*/
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
useEnhancedProtection?: boolean;
useCanvasRendering?: boolean;
allowDownloads?: boolean;
/**
* Compact chip row instead of cover cards. Used by the full-bleed layouts
@@ -40,8 +38,6 @@ export const GalleryFolderTiles: React.FC<GalleryFolderTilesProps> = ({
onOpen,
compact = false,
slug,
protectionLevel,
useCanvasRendering,
}) => {
const { t } = useTranslation();
@@ -95,10 +91,6 @@ export const GalleryFolderTiles: React.FC<GalleryFolderTilesProps> = ({
isGallery
slug={slug}
// Same rule as every other gallery image path: maximum
// protection implies canvas rendering even when the separate
// toggle is off (its default), otherwise a cover silently
// falls back to a blob-backed <img>.
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
/>
) : (
<div className="w-full h-full flex items-center justify-center">
@@ -1311,9 +1311,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event, requiresP
onOpen={openFolderBySlug}
compact={compact}
slug={slug}
protectionLevel={protectionLevel}
useEnhancedProtection={protectionLevel !== 'basic'}
useCanvasRendering={useCanvasRendering}
allowDownloads={allowDownloads}
/>
);
@@ -23,9 +23,7 @@ interface HeroHeaderProps {
heroLogoPosition?: 'top' | 'center' | 'bottom';
dividerStyle?: HeroDividerStyle;
allowDownloads?: boolean;
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
useEnhancedProtection?: boolean;
useCanvasRendering?: boolean;
onScrollToContent?: () => void;
// Hero image anchor position (#162) keyword or "X% Y%" focal point
heroImageAnchor?: string;
@@ -43,8 +41,6 @@ export const HeroHeader: React.FC<HeroHeaderProps> = ({
heroLogoSize = 'medium',
heroLogoPosition = 'top',
dividerStyle = 'wave',
protectionLevel = 'standard',
useCanvasRendering = false,
onScrollToContent,
heroImageAnchor = 'center'
}) => {
@@ -142,7 +138,6 @@ export const HeroHeader: React.FC<HeroHeaderProps> = ({
style={{ objectPosition: heroImageAnchor }}
isGallery={true}
slug={slug}
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
/>
{/* Overlay */}
@@ -196,7 +196,6 @@ export const PhotoGrid: React.FC<PhotoGridProps> = ({
allowDownloads={allowDownloads}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering}
slug={slug}
feedbackEnabled={feedbackEnabled}
/>
@@ -214,6 +213,7 @@ export const PhotoGrid: React.FC<PhotoGridProps> = ({
allowDownloads={allowDownloads}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering}
disableRightClick={disableRightClick}
enableDevtoolsProtection={enableDevtoolsProtection}
/>
@@ -231,7 +231,6 @@ interface PhotoThumbnailProps {
allowDownloads?: boolean;
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
useEnhancedProtection?: boolean;
useCanvasRendering?: boolean;
slug: string; // Add slug as required prop
feedbackEnabled?: boolean;
}
@@ -244,7 +243,6 @@ const PhotoThumbnail: React.FC<PhotoThumbnailProps> = ({
onDownload,
allowDownloads = true,
protectionLevel = 'standard',
useCanvasRendering = false,
slug,
feedbackEnabled = false
}) => {
@@ -268,7 +266,6 @@ const PhotoThumbnail: React.FC<PhotoThumbnailProps> = ({
loading="lazy"
isGallery={true}
slug={slug}
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
onProtectionViolation={(violationType) => {
// Track analytics
if (typeof window !== 'undefined' && (window as any).umami) {
@@ -361,9 +361,7 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
heroLogoPosition={heroLogoPosition}
dividerStyle={heroDividerStyle}
allowDownloads={allowDownloads}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering}
heroImageAnchor={heroImageAnchor}
/>
)}
@@ -0,0 +1,56 @@
/**
* Canvas rendering is a lightbox concern, not a tile concern.
*
* A canvas pins a backing store of naturalWidth × naturalHeight × 4 bytes
* that the browser is not allowed to evict, and iOS Safari has a hard
* budget for canvas memory that fails silently when exceeded — blank
* tiles, no error. A gallery is hundreds of tiles and one lightbox image,
* so the tiles render <img> whatever the protection level says, and the
* lightbox keeps the per-event toggle and the `maximum` implication.
*
* Source-level pin: nothing under components/gallery except PhotoLightbox
* may hand `useCanvasRendering` to AuthenticatedImage.
*/
import fs from 'fs';
import path from 'path';
import { describe, it, expect } from 'vitest';
const root = path.resolve(__dirname, '..');
function walk(dir: string): string[] {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const file = path.join(dir, entry.name);
if (entry.isDirectory()) return entry.name === '__tests__' ? [] : walk(file);
return file.endsWith('.tsx') ? [file] : [];
});
}
describe('canvas rendering stays in the lightbox', () => {
const files = walk(root);
const lightbox = path.join(root, 'PhotoLightbox.tsx');
/** The JSX props of every <AuthenticatedImage> in a file, plus every
* `imageProps={{ ... }}` object a layout hands to PhotoCard to spread in. */
const imageProps = (source: string) => [
...source.split('<AuthenticatedImage').slice(1).map((chunk) => chunk.split('/>')[0]),
...source.split('imageProps={{').slice(1).map((chunk) => chunk.split('}}')[0]),
];
it('only PhotoLightbox passes useCanvasRendering to AuthenticatedImage', () => {
const offenders = files.filter((file) => file !== lightbox
&& imageProps(fs.readFileSync(file, 'utf8')).some((props) => props.includes('useCanvasRendering')));
expect(offenders.map((f) => path.relative(root, f))).toEqual([]);
// The pin has teeth: the lightbox itself is caught by the same probe.
expect(imageProps(fs.readFileSync(lightbox, 'utf8')).some((props) => props.includes('useCanvasRendering'))).toBe(true);
});
it('only PhotoLightbox turns canvas on for protection level maximum', () => {
const offenders = files.filter((file) => file !== lightbox
&& /useCanvasRendering[^\n]*protectionLevel === 'maximum'/.test(fs.readFileSync(file, 'utf8')));
expect(offenders.map((f) => path.relative(root, f))).toEqual([]);
});
it('the lightbox still does both', () => {
const source = fs.readFileSync(lightbox, 'utf8');
expect(source).toMatch(/useCanvasRendering=\{useCanvasRendering \|\| protectionLevel === 'maximum'\}/);
});
});
@@ -47,6 +47,7 @@ export interface BaseGalleryLayoutProps {
onPickResolution?: (photoIds: number[]) => void;
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
useEnhancedProtection?: boolean;
/** Canvas rendering applies to the lightbox only; tiles always render <img>. */
useCanvasRendering?: boolean;
feedbackEnabled?: boolean;
feedbackOptions?: {
@@ -46,9 +46,7 @@ interface PhotoCardProps {
isLiked: boolean;
slug: string;
allowDownloads?: boolean;
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
useEnhancedProtection?: boolean;
useCanvasRendering?: boolean;
feedbackEnabled?: boolean;
// #506: track the per-event "allow likes" toggle so the per-photo
// Like button respects it. `feedbackEnabled` alone isn't enough —
@@ -68,8 +66,6 @@ const PhotoCard: React.FC<PhotoCardProps> = ({
isSelectionMode,
isLiked,
slug,
protectionLevel = 'standard',
useCanvasRendering = false,
feedbackEnabled = false,
allowLikes = false,
index
@@ -122,7 +118,6 @@ const PhotoCard: React.FC<PhotoCardProps> = ({
loading="lazy"
isGallery={true}
slug={slug}
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
/>
{/* Colour label (#1044) — same badge every layout uses. */}
@@ -208,9 +203,7 @@ export const GalleryPremiumLayout: React.FC<GalleryPremiumLayoutProps> = ({
allowDownloads = true,
downloadChoices,
onPickResolution,
protectionLevel = 'standard',
useEnhancedProtection = false,
useCanvasRendering = false,
feedbackEnabled = false,
feedbackOptions,
heroPhotoOverride,
@@ -629,9 +622,7 @@ export const GalleryPremiumLayout: React.FC<GalleryPremiumLayoutProps> = ({
isLiked={likedPhotoIds.has(originalPhoto.id)}
slug={slug}
allowDownloads={allowDownloads}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering}
feedbackEnabled={feedbackEnabled}
allowLikes={!!feedbackOptions?.allowLikes}
index={photoIndex}
@@ -255,9 +255,7 @@ export const GalleryStoryLayout: React.FC<GalleryStoryLayoutProps> = ({
photo={heroPhoto}
slug={slug}
allowDownloads={allowDownloads}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering}
/>
{/* Main Content - Scenes */}
@@ -281,9 +279,7 @@ export const GalleryStoryLayout: React.FC<GalleryStoryLayoutProps> = ({
onPhotoClick={handleOpenLightbox}
slug={slug}
allowDownloads={allowDownloads}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering}
/>
) : (
<div id={`gallery-${scene.id}`} className="story-gallery-grid">
@@ -298,9 +294,7 @@ export const GalleryStoryLayout: React.FC<GalleryStoryLayoutProps> = ({
slug={slug}
galleryId={`gallery-${scene.id}`}
allowDownloads={allowDownloads}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering}
// Mark first photo in each grid as featured
featured={index === 0 && scene.photos.length > 4}
/>
@@ -18,9 +18,7 @@ interface GridPhotoProps {
animationType?: string;
allowDownloads?: boolean;
slug?: string;
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
useEnhancedProtection?: boolean;
useCanvasRendering?: boolean;
feedbackEnabled?: boolean;
feedbackOptions?: {
allowLikes?: boolean;
@@ -47,8 +45,6 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
animationType = 'fade',
allowDownloads = true,
slug,
protectionLevel = 'standard',
useCanvasRendering = false,
feedbackEnabled = false,
feedbackOptions,
savedIdentity,
@@ -130,7 +126,6 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
loading: 'lazy',
isGallery: true,
slug,
useCanvasRendering: useCanvasRendering || protectionLevel === 'maximum',
onProtectionViolation: (violationType: string) => {
console.warn(`Protection violation on grid photo ${photo.id}: ${violationType}`);
},
@@ -200,9 +195,7 @@ export const GridGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
isSelectionMode = false,
onPhotoSelect,
allowDownloads = true,
protectionLevel = 'standard',
useEnhancedProtection = false,
useCanvasRendering = false,
feedbackEnabled = false,
feedbackOptions,
isClient = false,
@@ -255,9 +248,7 @@ export const GridGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
animationType={animation}
allowDownloads={allowDownloads}
slug={slug}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering}
feedbackEnabled={feedbackEnabled}
feedbackOptions={feedbackOptions}
savedIdentity={savedIdentity}
@@ -40,9 +40,7 @@ interface JustifiedPhotoProps {
animationType?: string;
allowDownloads?: boolean;
slug?: string;
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
useEnhancedProtection?: boolean;
useCanvasRendering?: boolean;
feedbackEnabled?: boolean;
feedbackOptions?: {
allowLikes?: boolean;
@@ -69,8 +67,6 @@ const JustifiedPhoto: React.FC<JustifiedPhotoProps> = ({
animationType = 'fade',
allowDownloads = true,
slug,
protectionLevel = 'standard',
useCanvasRendering = false,
feedbackEnabled = false,
feedbackOptions,
savedIdentity,
@@ -133,7 +129,6 @@ const JustifiedPhoto: React.FC<JustifiedPhotoProps> = ({
loading: 'lazy',
isGallery: true,
slug,
useCanvasRendering: useCanvasRendering || protectionLevel === 'maximum',
onProtectionViolation: (violationType: string) => {
console.warn(`Protection violation on justified photo ${photo.id}: ${violationType}`);
},
@@ -214,9 +209,7 @@ export const JustifiedGalleryLayout: React.FC<JustifiedGalleryLayoutProps> = ({
isSelectionMode = false,
onPhotoSelect,
allowDownloads = true,
protectionLevel = 'standard',
useEnhancedProtection = false,
useCanvasRendering = false,
feedbackEnabled = false,
feedbackOptions,
// Hero props
@@ -399,7 +392,6 @@ export const JustifiedGalleryLayout: React.FC<JustifiedGalleryLayoutProps> = ({
className="w-full h-full object-cover"
isGallery={true}
slug={slug}
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
/>
{/* Overlay */}
@@ -523,9 +515,7 @@ export const JustifiedGalleryLayout: React.FC<JustifiedGalleryLayoutProps> = ({
animationType={animation}
allowDownloads={allowDownloads}
slug={slug}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering}
feedbackEnabled={feedbackEnabled}
feedbackOptions={feedbackOptions}
savedIdentity={savedIdentity}
@@ -14,9 +14,7 @@ interface StoryCarouselProps {
slug: string;
id: string;
allowDownloads?: boolean;
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
useEnhancedProtection?: boolean;
useCanvasRendering?: boolean;
}
export const StoryCarousel: React.FC<StoryCarouselProps> = ({
@@ -27,9 +25,7 @@ export const StoryCarousel: React.FC<StoryCarouselProps> = ({
slug,
id,
allowDownloads = true,
protectionLevel = 'standard',
useEnhancedProtection = false,
useCanvasRendering = false
}) => {
return (
<div id={id} className="story-carousel">
@@ -53,9 +49,7 @@ export const StoryCarousel: React.FC<StoryCarouselProps> = ({
slug={slug}
galleryId={id}
allowDownloads={allowDownloads}
protectionLevel={protectionLevel}
useEnhancedProtection={useEnhancedProtection}
useCanvasRendering={useCanvasRendering}
/>
</div>
</SwiperSlide>
@@ -11,9 +11,7 @@ interface StoryHeroProps {
photo?: Photo | null;
slug: string;
allowDownloads?: boolean;
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
useEnhancedProtection?: boolean;
useCanvasRendering?: boolean;
}
export const StoryHero: React.FC<StoryHeroProps> = ({
@@ -22,8 +20,6 @@ export const StoryHero: React.FC<StoryHeroProps> = ({
stats,
photo,
slug,
protectionLevel = 'standard',
useCanvasRendering = false
}) => {
const formattedDate = date
? new Date(date).toLocaleDateString('en-US', {
@@ -49,7 +45,6 @@ export const StoryHero: React.FC<StoryHeroProps> = ({
className="w-full h-full object-cover"
isGallery={true}
slug={slug}
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
/>
) : (
<div className="w-full h-full bg-gray-900" />
@@ -14,9 +14,7 @@ interface StoryPhotoCardProps {
onClick?: () => void;
slug: string;
allowDownloads?: boolean;
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
useEnhancedProtection?: boolean;
useCanvasRendering?: boolean;
featured?: boolean;
galleryId: string;
}
@@ -28,8 +26,6 @@ export const StoryPhotoCard: React.FC<StoryPhotoCardProps> = ({
onToggleFavorite,
onClick,
slug,
protectionLevel = 'standard',
useCanvasRendering = false,
featured = false,
galleryId: _galleryId
}) => {
@@ -103,7 +99,6 @@ export const StoryPhotoCard: React.FC<StoryPhotoCardProps> = ({
}`}
isGallery={true}
slug={slug}
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
/>
)}
</a>
@@ -184,7 +184,7 @@ export const ImageSecurityTab: React.FC = () => {
/>
<Image className="w-4 h-4 ml-2 mr-1 text-neutral-500" />
<span className="text-sm text-neutral-700 dark:text-neutral-300">
{t('settings.imageSecurity.enableCanvas', 'Enable canvas rendering by default (advanced protection)')}
{t('settings.imageSecurity.enableCanvas', 'Enable canvas rendering in the lightbox by default (advanced protection)')}
</span>
</label>
</div>
+2 -2
View File
@@ -1934,7 +1934,7 @@
"disableRightClick": "Rechtsklick-Menü blockieren",
"watermarkDownloads": "Wasserzeichen bei Downloads hinzufügen",
"enableDevtoolsProtection": "Entwicklertools erkennen",
"useCanvasRendering": "Canvas-Rendering (erweiterter Schutz)",
"useCanvasRendering": "Canvas-Rendering in der Lightbox (erweiterter Schutz)",
"protectionInfo": "Schutzfunktionen helfen, unerlaubte Downloads zu verhindern, können jedoch nicht alle Methoden blockieren.",
"protectionLevelBasic": "Einfach - Nur Rechtsklick-Sperre",
"protectionLevelStandard": "Standard - Tastenkombinationen blockiert",
@@ -2374,7 +2374,7 @@
"protectionLevel": "Standard-Schutzstufe",
"imageQuality": "Standard-Bildqualität",
"enableDevtools": "DevTools-Erkennung standardmäßig aktivieren",
"enableCanvas": "Canvas-Rendering standardmäßig aktivieren (erweiterter Schutz)",
"enableCanvas": "Canvas-Rendering in der Lightbox standardmäßig aktivieren (erweiterter Schutz)",
"rateLimiting": "Ratenbegrenzung",
"rateLimitingHelp": "Begrenzen Sie, wie viele Bilder angefordert werden können, um Scraping zu verhindern.",
"requestsPerMinute": "Anfragen pro Minute",
+2 -2
View File
@@ -1430,7 +1430,7 @@
"disableRightClick": "Block right-click menu",
"watermarkDownloads": "Add watermark to downloads",
"enableDevtoolsProtection": "Detect developer tools",
"useCanvasRendering": "Canvas rendering (advanced protection)",
"useCanvasRendering": "Canvas rendering in the lightbox (advanced protection)",
"protectionInfo": "Protection features help prevent unauthorized downloads but cannot block all methods.",
"protectionLevelBasic": "Basic - Right-click blocking only",
"protectionLevelStandard": "Standard - Keyboard shortcuts blocked",
@@ -1937,7 +1937,7 @@
"protectionLevel": "Default Protection Level",
"imageQuality": "Default Image Quality",
"enableDevtools": "Enable DevTools detection by default",
"enableCanvas": "Enable canvas rendering by default (advanced protection)",
"enableCanvas": "Enable canvas rendering in the lightbox by default (advanced protection)",
"rateLimiting": "Rate Limiting",
"rateLimitingHelp": "Limit how many images can be requested to prevent scraping.",
"requestsPerMinute": "Requests per minute",
+2 -2
View File
@@ -498,7 +498,7 @@
"disableRightClick": "Bloquer le clic droit",
"watermarkDownloads": "Ajouter un filigrane aux téléchargements",
"enableDevtoolsProtection": "Détecter les outils de développement",
"useCanvasRendering": "Rendu canvas (protection avancée)",
"useCanvasRendering": "Rendu canvas dans la visionneuse (protection avancée)",
"protectionInfo": "Les fonctionnalités de protection aident à prévenir les téléchargements non autorisés mais ne peuvent pas bloquer toutes les méthodes.",
"protectionLevelBasic": "Basique - Blocage du clic droit uniquement",
"protectionLevelStandard": "Standard - Blocage des raccourcis clavier",
@@ -912,7 +912,7 @@
"protectionLevel": "Niveau de protection par défaut",
"imageQuality": "Qualité d'image par défaut",
"enableDevtools": "Activer la détection des outils DevTools par défaut",
"enableCanvas": "Activer le rendu canvas par défaut (protection avancée)",
"enableCanvas": "Activer le rendu canvas dans la visionneuse par défaut (protection avancée)",
"rateLimiting": "Limitation de débit",
"rateLimitingHelp": "Limitez le nombre d'images qui peuvent être demandées pour éviter le scraping.",
"requestsPerMinute": "Requêtes par minute",
+2 -2
View File
@@ -498,7 +498,7 @@
"disableRightClick": "Blokiraj meni desnega klika",
"watermarkDownloads": "Dodaj vodni žig prenosom",
"enableDevtoolsProtection": "Zaznaj orodja za razvijalce",
"useCanvasRendering": "Izris prek canvas (napredna zaščita)",
"useCanvasRendering": "Izris prek canvas v pregledovalniku (napredna zaščita)",
"protectionInfo": "Zaščitne funkcije pomagajo preprečiti nepooblaščene prenose, vendar ne morejo blokirati vseh metod.",
"protectionLevelBasic": "Osnovno - samo blokada desnega klika",
"protectionLevelStandard": "Standardno - blokirane bližnjice na tipkovnici",
@@ -912,7 +912,7 @@
"protectionLevel": "Privzeta raven zaščite",
"imageQuality": "Privzeta kakovost slike",
"enableDevtools": "Privzeto omogoči zaznavanje DevTools",
"enableCanvas": "Privzeto omogoči izris prek canvas (napredna zaščita)",
"enableCanvas": "Privzeto omogoči izris prek canvas v pregledovalniku (napredna zaščita)",
"rateLimiting": "Omejevanje hitrosti",
"rateLimitingHelp": "Omejite, koliko slik je mogoče zahtevati, da preprečite množično pobiranje.",
"requestsPerMinute": "Zahtev na minuto",
@@ -669,7 +669,7 @@ export const EventInformationCard: React.FC<EventInformationCardProps> = ({
className="w-4 h-4 text-accent border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500"
/>
<Image className="w-4 h-4 ml-2 mr-1 text-neutral-500 dark:text-neutral-400" />
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('events.useCanvasRendering', 'Canvas rendering (advanced protection)')}</span>
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('events.useCanvasRendering', 'Canvas rendering in the lightbox (advanced protection)')}</span>
</label>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-2">