diff --git a/backend/migrations/core/074_photo_visibility_client_access.js b/backend/migrations/core/074_photo_visibility_client_access.js new file mode 100644 index 00000000..b4c42f1e --- /dev/null +++ b/backend/migrations/core/074_photo_visibility_client_access.js @@ -0,0 +1,28 @@ +const { addColumnIfNotExists, createIndexIfNotExists } = require('../helpers'); + +exports.up = async function(knex) { + // Add visibility column to photos table + await addColumnIfNotExists(knex, 'photos', 'visibility', (table) => { + table.string('visibility', 20).defaultTo('visible').notNullable(); + }); + + // Add client access columns to events table + await addColumnIfNotExists(knex, 'events', 'client_access_enabled', (table) => { + table.boolean('client_access_enabled').defaultTo(false); + }); + + await addColumnIfNotExists(knex, 'events', 'client_password_hash', (table) => { + table.string('client_password_hash', 255).nullable(); + }); + + await addColumnIfNotExists(knex, 'events', 'client_share_token', (table) => { + table.string('client_share_token', 64).nullable().unique(); + }); + + // Index for filtering photos by visibility + await createIndexIfNotExists(knex, 'photos', ['event_id', 'visibility'], 'idx_photos_event_visibility'); +}; + +exports.down = async function(knex) { + // Safe rollback - intentionally no-op to avoid data loss +}; diff --git a/backend/src/middleware/gallery.js b/backend/src/middleware/gallery.js index c809d7af..fb9ab8ad 100644 --- a/backend/src/middleware/gallery.js +++ b/backend/src/middleware/gallery.js @@ -102,8 +102,9 @@ async function verifyGalleryAccess(req, res, next) { logger.debug('[verifyGalleryAccess] Event located', { eventId: event.id, slug: event.slug }); req.event = event; + req.accessLevel = decoded.accessLevel || 'guest'; req.sessionID = decoded.sessionId || `gallery_${event.id}_${Date.now()}`; - + // Create client info for logging (similar to secureImageMiddleware but simpler) req.clientInfo = { ip: req.ip || req.connection.remoteAddress || 'unknown', diff --git a/backend/src/routes/adminEvents.js b/backend/src/routes/adminEvents.js index 07d86368..935b30db 100644 --- a/backend/src/routes/adminEvents.js +++ b/backend/src/routes/adminEvents.js @@ -124,6 +124,8 @@ const mapEventForApi = (event) => { host_email, customer_name, customer_email, + password_hash: _ph, + client_password_hash: _cph, ...rest } = event; @@ -208,7 +210,10 @@ router.post('/', adminAuth, requirePermission('events.create'), [ body('header_style').optional().isIn(['hero', 'standard', 'minimal', 'none']), body('hero_divider_style').optional().isIn(['wave', 'straight', 'angle', 'curve', 'none']), // Hero image anchor position (#162) – accepts legacy keywords or "X% Y%" focal point - body('hero_image_anchor').optional().custom(validateHeroImageAnchor) + body('hero_image_anchor').optional().custom(validateHeroImageAnchor), + // Client access settings (#172) + body('client_access_enabled').optional().isBoolean(), + body('client_password').optional().isString() ], async (req, res) => { try { logger.debug('Create event request body', { body: req.body }); @@ -258,7 +263,10 @@ router.post('/', adminAuth, requirePermission('events.create'), [ // Hero image anchor position (#162) hero_image_anchor = 'center', // Photo cap - photo_cap = null + photo_cap = null, + // Client access settings (#172) + client_access_enabled = false, + client_password = null } = req.body; const customerName = getCustomerNameFromPayload(req.body); @@ -419,7 +427,13 @@ router.post('/', adminAuth, requirePermission('events.create'), [ header_style: effectiveHeaderStyle || 'standard', hero_divider_style: effectiveDividerStyle || 'wave', hero_image_anchor: hero_image_anchor || 'center', - photo_cap: photo_cap || null + photo_cap: photo_cap || null, + // Client access (#172) + client_access_enabled: formatBoolean(client_access_enabled), + ...(client_access_enabled && client_password ? { + client_password_hash: await bcrypt.hash(client_password, getBcryptRounds()), + client_share_token: crypto.randomBytes(32).toString('hex') + } : {}) }).returning('id'); // Handle both PostgreSQL (returns array of objects) and SQLite (returns array of IDs) @@ -453,21 +467,32 @@ router.post('/', adminAuth, requirePermission('events.create'), [ // Language detection is handled by email processor if (customerEmail) { + // Build email data with optional client access info + const emailData = { + customer_name: customerName, + customer_email: customerEmail, + host_name: customerName || (customerEmail ? customerEmail.split('@')[0] : null), + event_name, + event_date: event_date, // Pass raw date - will be formatted by email processor + gallery_link: shareUrl, + gallery_password: requirePassword ? password : 'No password required', + expiry_date: expires_at ? expires_at.toISOString() : null, // Pass ISO string - will be formatted by email processor + welcome_message: welcome_message || '' + }; + + // Include client access info in email when enabled (#172) + if (client_access_enabled && client_password) { + const createdEvent = await db('events').where('id', eventId).first(); + const frontendUrl = process.env.FRONTEND_URL || process.env.APP_URL || ''; + emailData.client_link = `${frontendUrl}/gallery/${slug}/client-access?token=${createdEvent.client_share_token}`; + emailData.client_password = client_password; + } + await db('email_queue').insert({ event_id: eventId, recipient_email: customerEmail, email_type: 'gallery_created', - email_data: JSON.stringify({ - customer_name: customerName, - customer_email: customerEmail, - host_name: customerName || (customerEmail ? customerEmail.split('@')[0] : null), - event_name, - event_date: event_date, // Pass raw date - will be formatted by email processor - gallery_link: shareUrl, - gallery_password: requirePassword ? password : 'No password required', - expiry_date: expires_at ? expires_at.toISOString() : null, // Pass ISO string - will be formatted by email processor - welcome_message: welcome_message || '' - }), + email_data: JSON.stringify(emailData), status: 'pending', created_at: new Date() // scheduled_at will use default value @@ -709,7 +734,11 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), [ body('header_style').optional().isIn(['hero', 'standard', 'minimal', 'none']), body('hero_divider_style').optional().isIn(['wave', 'straight', 'angle', 'curve', 'none']), // Hero image anchor position (#162) – accepts legacy keywords or "X% Y%" focal point - body('hero_image_anchor').optional().custom(validateHeroImageAnchor) + body('hero_image_anchor').optional().custom(validateHeroImageAnchor), + // Client access settings (#172) + body('client_access_enabled').optional().isBoolean(), + body('client_password').optional().isString(), + body('regenerate_client_token').optional().isBoolean() ], async (req, res) => { try { const errors = validationResult(req); @@ -788,6 +817,25 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), [ return res.status(400).json({ error: 'external_path is required when source_mode is reference' }); } + // Handle client access fields (#172) + if (Object.prototype.hasOwnProperty.call(updates, 'client_access_enabled')) { + updates.client_access_enabled = formatBoolean(updates.client_access_enabled); + // Auto-generate client share token when first enabling + if (parseBooleanInput(updates.client_access_enabled, false) && !event.client_share_token) { + updates.client_share_token = crypto.randomBytes(32).toString('hex'); + } + } + if (Object.prototype.hasOwnProperty.call(updates, 'client_password') && updates.client_password) { + updates.client_password_hash = await bcrypt.hash(updates.client_password, getBcryptRounds()); + delete updates.client_password; + } else { + delete updates.client_password; + } + if (updates.regenerate_client_token) { + updates.client_share_token = crypto.randomBytes(32).toString('hex'); + } + delete updates.regenerate_client_token; + // Log the update request for debugging logger.debug('Update event request', { id, diff --git a/backend/src/routes/adminPhotos.js b/backend/src/routes/adminPhotos.js index f48d2bb5..5aaeaaf6 100644 --- a/backend/src/routes/adminPhotos.js +++ b/backend/src/routes/adminPhotos.js @@ -587,7 +587,7 @@ router.delete('/:eventId/photos/:photoId', adminAuth, requirePermission('photos. router.patch('/:eventId/photos/:photoId', adminAuth, requirePermission('photos.edit'), async (req, res) => { try { const { eventId, photoId } = req.params; - const { category_id } = req.body; + const { category_id, visibility } = req.body; // Verify photo belongs to event const photo = await db('photos') @@ -601,6 +601,13 @@ router.patch('/:eventId/photos/:photoId', adminAuth, requirePermission('photos.e // Prepare update data const updateData = {}; + // Handle visibility update (#172) + if (visibility !== undefined) { + if (['visible', 'hidden'].includes(visibility)) { + updateData.visibility = visibility; + } + } + // Handle type-based categories ('individual' or 'collage') // These are string values that map to the photo.type field if (category_id === 'individual' || category_id === 'collage') { @@ -736,6 +743,13 @@ router.post('/:eventId/photos/bulk-update', adminAuth, requirePermission('photos // Prepare update data const updateData = {}; + // Handle visibility update (#172) + if (updates.visibility !== undefined) { + if (['visible', 'hidden'].includes(updates.visibility)) { + updateData.visibility = updates.visibility; + } + } + if (updates.category_id !== undefined) { // Handle type-based categories ('individual' or 'collage') // These are string values that map to the photo.type field diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index bfc8b704..229f5bc1 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -290,6 +290,82 @@ router.post('/gallery/verify', [ } }); +// Client access login (PIN-based) +router.post('/gallery/:slug/client-login', [ + body('password').notEmpty().isString() +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { slug } = req.params; + const { password } = req.body; + const ipAddress = getClientIp(req); + const userAgent = req.headers['user-agent'] || ''; + + const event = await db('events') + .where({ slug, is_active: formatBoolean(true), is_archived: formatBoolean(false) }) + .first(); + + if (!event || !event.client_access_enabled || !event.client_password_hash) { + await trackFailedAttempt(`client:${slug}`, ipAddress, userAgent); + return res.status(401).json({ error: 'Invalid credentials' }); + } + + const lockoutStatus = await checkAccountLockout(`client:${slug}`, ipAddress); + if (lockoutStatus.isLocked) { + return res.status(423).json({ + error: 'Too many failed attempts. Please try again later.', + retryAfter: lockoutStatus.remainingTime + }); + } + + const validPassword = await bcrypt.compare(password, event.client_password_hash); + if (!validPassword) { + await trackFailedAttempt(`client:${slug}`, ipAddress, userAgent); + return res.status(401).json({ error: 'Invalid credentials' }); + } + + await trackSuccessfulLogin(`client:${slug}`, ipAddress, userAgent); + + const token = jwt.sign({ + eventId: event.id, + eventSlug: event.slug, + type: 'gallery', + accessLevel: 'client', + ip: ipAddress, + loginTime: Date.now() + }, process.env.JWT_SECRET, { + expiresIn: '24h', + issuer: 'picpeak-auth' + }); + + setGalleryAuthCookies(res, token, event.slug); + + res.json({ + token, + event: { + id: event.id, + event_name: event.event_name, + event_type: event.event_type, + event_date: event.event_date, + welcome_message: event.welcome_message, + color_theme: event.color_theme, + expires_at: event.expires_at, + allow_user_uploads: event.allow_user_uploads, + upload_category_id: event.upload_category_id, + require_password: true + }, + accessLevel: 'client' + }); + } catch (error) { + logger.error('Client login error:', error); + res.status(500).json({ error: 'Authentication failed' }); + } +}); + // Share link authentication (token-based) router.post('/gallery/share-login', [ body('slug').notEmpty().trim(), diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index b5af23b8..cc41c082 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -198,10 +198,18 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => { // Build the query with sorting const sortOrder = order === 'asc' ? 'asc' : 'desc'; + const isClient = req.accessLevel === 'client'; let photosQuery = db('photos') .where('photos.event_id', req.event.id) .select('photos.*'); + // Guests only see visible photos; clients see all + if (!isClient) { + photosQuery = photosQuery.where(function() { + this.where('photos.visibility', 'visible').orWhereNull('photos.visibility'); + }); + } + // Apply sort option if (sort === 'capture_date') { // Sort by capture date, falling back to uploaded_at if capture date is null @@ -414,12 +422,20 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => { height: photo.height || null, // Fixed: Use the calculated useJwtUrl variable instead of recalculating requires_token: !useJwtUrl, + // EXIF capture date + captured_at: photo.captured_at || null, + // Media type + media_type: photo.media_type || null, + mime_type: photo.mime_type || null, + duration: photo.duration || null, // Feedback data has_feedback: (commentMap[photo.id] > 0 || photo.average_rating > 0 || photo.like_count > 0), average_rating: photo.average_rating || 0, comment_count: commentMap[photo.id] || 0, like_count: photo.like_count || 0, - favorite_count: photo.favorite_count || 0 + favorite_count: photo.favorite_count || 0, + // Visibility (only included for clients) + ...(isClient ? { visibility: photo.visibility || 'visible' } : {}) }; }) }); @@ -429,24 +445,91 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => { } }); +// Toggle photo visibility (client-only) +router.patch('/:slug/photos/:photoId/visibility', verifyGalleryAccess, async (req, res) => { + try { + if (req.accessLevel !== 'client') { + return res.status(403).json({ error: 'Client access required' }); + } + + const { photoId } = req.params; + const { visibility } = req.body; + + if (!['visible', 'hidden'].includes(visibility)) { + return res.status(400).json({ error: 'Invalid visibility value' }); + } + + const photo = await db('photos') + .where({ id: photoId, event_id: req.event.id }) + .first(); + + if (!photo) { + return res.status(404).json({ error: 'Photo not found' }); + } + + await db('photos') + .where({ id: photoId, event_id: req.event.id }) + .update({ visibility }); + + res.json({ message: 'Photo visibility updated', visibility }); + } catch (error) { + logger.error('Error updating photo visibility:', error); + res.status(500).json({ error: 'Failed to update photo visibility' }); + } +}); + +// Bulk toggle photo visibility (client-only) +router.patch('/:slug/photos/visibility/bulk', verifyGalleryAccess, async (req, res) => { + try { + if (req.accessLevel !== 'client') { + return res.status(403).json({ error: 'Client access required' }); + } + + const { photoIds, visibility } = req.body; + + if (!Array.isArray(photoIds) || photoIds.length === 0) { + return res.status(400).json({ error: 'Invalid photo IDs' }); + } + + if (!['visible', 'hidden'].includes(visibility)) { + return res.status(400).json({ error: 'Invalid visibility value' }); + } + + const count = await db('photos') + .whereIn('id', photoIds) + .where('event_id', req.event.id) + .update({ visibility }); + + res.json({ message: `${count} photos updated`, visibility }); + } catch (error) { + logger.error('Error bulk updating photo visibility:', error); + res.status(500).json({ error: 'Failed to update photo visibility' }); + } +}); + // Download single photo router.get('/:slug/download/:photoId', verifyGalleryAccess, async (req, res) => { try { const { photoId } = req.params; - + // Check if downloads are allowed for this event if (req.event.allow_downloads === false) { return res.status(403).json({ error: 'Downloads are disabled for this gallery' }); } - + const photo = await db('photos') .where({ id: photoId, event_id: req.event.id }) .first(); - + if (!photo) { return res.status(404).json({ error: 'Photo not found' }); } - + + // Block guest access to hidden photos + if (photo.visibility === 'hidden' && req.accessLevel !== 'client') { + return res.status(403).json({ error: 'Photo not available' }); + } + // Update download count await db('photos').where('id', photoId).increment('download_count', 1); @@ -745,11 +828,15 @@ router.get('/:slug/photo/:photoId', .where({ id: photoId, event_id: req.event.id }) .first(); - if (!photo) { return res.status(404).json({ error: 'Photo not found' }); } + // Block guest access to hidden photos + if (photo.visibility === 'hidden' && req.accessLevel !== 'client') { + return res.status(403).json({ error: 'Photo not available' }); + } + // Check if this is a video const isVideo = photo.media_type === 'video' || (photo.mime_type && photo.mime_type.startsWith('video/')); @@ -935,6 +1022,11 @@ router.get('/:slug/thumbnail/:photoId', return res.status(404).json({ error: 'Photo not found' }); } + // Block guest access to hidden photos + if (photo.visibility === 'hidden' && req.accessLevel !== 'client') { + return res.status(403).json({ error: 'Photo not available' }); + } + // Ensure thumbnail exists and is valid, regenerate if needed const thumbnailPath = await ensureThumbnail(photo); @@ -1013,6 +1105,11 @@ router.get('/:slug/hero/:photoId', return res.status(404).json({ error: 'Photo not found' }); } + // Block guest access to hidden photos + if (photo.visibility === 'hidden' && req.accessLevel !== 'client') { + return res.status(403).json({ error: 'Photo not available' }); + } + // Check if this is a video - videos don't get hero images const isVideo = photo.media_type === 'video' || (photo.mime_type && photo.mime_type.startsWith('video/')); if (isVideo) { diff --git a/backend/src/services/emailProcessor.js b/backend/src/services/emailProcessor.js index 09e3d878..8381c5a7 100644 --- a/backend/src/services/emailProcessor.js +++ b/backend/src/services/emailProcessor.js @@ -353,6 +353,53 @@ async function processTemplate(template, variables, language = 'en') { htmlBody = htmlTemplate(processedVariables); textBody = textTemplate(processedVariables); + // Inject client access section if client_link is provided (#172) + if (processedVariables.client_link) { + const clientAccessI18n = { + de: { + label: 'Kundenzugang (Privat)', + desc: 'Fotos überprüfen und deren Sichtbarkeit festlegen, bevor die Galerie geteilt wird:', + link: 'Kundenzugang öffnen', + warning: 'Diesen Link nicht teilen — er ermöglicht das Ausblenden von Fotos in der Gästegalerie.', + }, + ru: { + label: 'Доступ клиента (Личный)', + desc: 'Просмотрите и управляйте видимостью фотографий перед тем, как поделиться галереей с гостями:', + link: 'Открыть доступ клиента', + warning: 'Не делитесь этой ссылкой — она позволяет скрывать фотографии из гостевой галереи.', + }, + pt: { + label: 'Acesso do Cliente (Privado)', + desc: 'Revise e gerencie a visibilidade das fotos antes de compartilhar com os convidados:', + link: 'Abrir Acesso do Cliente', + warning: 'Não compartilhe este link — ele permite ocultar fotos da galeria de convidados.', + }, + en: { + label: 'Client Access (Private)', + desc: 'Review and manage photo visibility before sharing with guests:', + link: 'Open Client Access', + warning: 'Do not share this link — it allows hiding photos from the guest gallery.', + }, + }; + const ci18n = clientAccessI18n[language] || clientAccessI18n.en; + const clientAccessLabel = ci18n.label; + const clientAccessDesc = ci18n.desc; + const clientAccessLink = ci18n.link; + const clientAccessWarning = ci18n.warning; + const pinLabel = 'PIN'; + + htmlBody += ` +
+ 🔒 ${clientAccessLabel} +

${clientAccessDesc}

+

+ ${clientAccessLink} +

+

${pinLabel}: ${processedVariables.client_password}

+

⚠️ ${clientAccessWarning}

+
`; + } + // Wrap HTML body in styled template const styledHtmlBody = await wrapEmailHtml(htmlBody, subject, language); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 70f267bb..688150bb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,6 +8,7 @@ import { analyticsService } from './services/analytics.service'; import { GalleryAuthProvider, MaintenanceProvider } from './contexts'; import { ThemeProvider } from './contexts/ThemeContext'; import { GalleryPage } from './pages/GalleryPage'; +import { ClientAccessPage } from './pages/ClientAccessPage'; import { PreviewPage } from './pages/gallery/PreviewPage'; import { LegalPage } from './pages/public/LegalPage'; import { @@ -121,6 +122,11 @@ function App() { {/* Public gallery routes */} } /> + + + + } /> diff --git a/frontend/src/components/admin/AdminPhotoGrid.tsx b/frontend/src/components/admin/AdminPhotoGrid.tsx index eab09e00..f7c8d8f5 100644 --- a/frontend/src/components/admin/AdminPhotoGrid.tsx +++ b/frontend/src/components/admin/AdminPhotoGrid.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Check, Download, Trash2, Eye, Package, MessageSquare, Star, Video, FolderOpen } from 'lucide-react'; +import { Check, Download, Trash2, Eye, EyeOff, Package, MessageSquare, Star, Video, FolderOpen } from 'lucide-react'; import { toast } from 'react-toastify'; import { useTranslation } from 'react-i18next'; @@ -201,6 +201,34 @@ export const AdminPhotoGrid: React.FC = ({ > {t('photos.moveToCategory', 'Move to Category')} + + + {/* Visibility badge (#172) */} + {(photo as any).visibility === 'hidden' && ( +
+ + + {t('admin.photos.hidden', 'Hidden')} + +
+ )} + {/* Thumbnail */}
{photo.thumbnail_url ? ( diff --git a/frontend/src/components/gallery/GalleryView.tsx b/frontend/src/components/gallery/GalleryView.tsx index 854528d1..ee1a49eb 100644 --- a/frontend/src/components/gallery/GalleryView.tsx +++ b/frontend/src/components/gallery/GalleryView.tsx @@ -17,11 +17,13 @@ import type { FilterType } from './GalleryFilter'; import { analyticsService } from '../../services/analytics.service'; import { useDevToolsProtection } from '../../hooks/useDevToolsProtection'; import { api } from '../../config/api'; -import { Upload, Menu } from 'lucide-react'; +import { Upload, Menu, Eye, EyeOff, Shield } from 'lucide-react'; import { galleryService } from '../../services/gallery.service'; import { useWatermarkSettings } from '../../hooks/useWatermarkSettings'; import { useGalleryCustomCss } from '../../hooks/useGalleryCustomCss'; import type { Photo } from '../../types'; +import { GALLERY_THEME_PRESETS } from '../../types/theme.types'; +import { useQueryClient } from '@tanstack/react-query'; interface GalleryViewProps { slug: string; @@ -42,8 +44,9 @@ interface GalleryViewProps { export const GalleryView: React.FC = ({ slug, event }) => { const { t } = useTranslation(); - const { logout } = useGalleryAuth(); - const { theme } = useTheme(); + const { logout, isClient } = useGalleryAuth(); + const { setTheme, theme } = useTheme(); + const queryClient = useQueryClient(); const [selectedCategoryId, setSelectedCategoryId] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [sortBy, setSortBy] = useState<'date' | 'name' | 'size' | 'rating' | 'capture_date'>('date'); @@ -274,6 +277,93 @@ export const GalleryView: React.FC = ({ slug, event }) => { setStaticHeroPhoto(defaultHeroPhoto); }, [selectedCategoryId, data?.categories, data?.photos, defaultHeroPhoto]); + // Apply theme when settings are loaded + useEffect(() => { + if (settingsData && data?.event) { + let themeToApply = null; + const fullEvent = data.event; // Use the full event data from API + + if (fullEvent.color_theme) { + try { + // Check if it's a valid JSON string + if (fullEvent.color_theme.startsWith('{')) { + const eventTheme = JSON.parse(fullEvent.color_theme); + themeToApply = eventTheme; + } else { + // Handle legacy theme names - check if it's a preset + const preset = GALLERY_THEME_PRESETS[fullEvent.color_theme]; + if (preset) { + themeToApply = preset.config; + } else { + // Unknown theme name, fall back to global theme + if (settingsData.theme_config) { + themeToApply = settingsData.theme_config; + } + } + } + } catch { + // Invalid theme format - use default + // Fall back to global theme + if (settingsData.theme_config) { + themeToApply = settingsData.theme_config; + } + } + } else if (settingsData.theme_config) { + // No event theme, use global theme + themeToApply = settingsData.theme_config; + } + + // Apply theme with a small delay to ensure it overrides any global theme + if (themeToApply) { + // Use setTimeout to ensure this runs after any global theme application + const timer = setTimeout(() => { + // If there's a hero photo, add it to gallery settings + if (fullEvent.hero_photo_id && themeToApply.gallerySettings) { + themeToApply.gallerySettings.heroImageId = fullEvent.hero_photo_id; + // Apply hero photo ID to existing gallery settings + } else if (fullEvent.hero_photo_id) { + themeToApply.gallerySettings = { heroImageId: fullEvent.hero_photo_id }; + // Create gallery settings with hero photo ID + } + setTheme(themeToApply); + }, 0); + + return () => clearTimeout(timer); + } + } + }, [settingsData, data, setTheme]); // Use data instead of event prop + + // Client visibility toggle handler (#172) + const handleToggleVisibility = async (photoId: number, currentVisibility: string) => { + const newVisibility = currentVisibility === 'hidden' ? 'visible' : 'hidden'; + try { + await galleryService.togglePhotoVisibility(slug, photoId, newVisibility); + queryClient.invalidateQueries({ queryKey: ['gallery-photos', slug] }); + } catch (error) { + console.error('Failed to toggle visibility:', error); + } + }; + + const handleBulkVisibility = async (visibility: 'visible' | 'hidden') => { + if (selectedPhotos.size === 0) return; + try { + await galleryService.bulkToggleVisibility(slug, Array.from(selectedPhotos), visibility); + setSelectedPhotos(new Set()); + setIsSelectionMode(false); + queryClient.invalidateQueries({ queryKey: ['gallery-photos', slug] }); + } catch (error) { + console.error('Failed to bulk toggle visibility:', error); + } + }; + + // Client visibility stats + const visibleCount = useMemo(() => { + if (!isClient || !data?.photos) return 0; + return data.photos.filter(p => p.visibility !== 'hidden').length; + }, [isClient, data?.photos]); + + const totalCount = data?.photos?.length || 0; + // Calculate days until expiration (null means never expires) const daysUntilExpiration = event.expires_at ? differenceInDays(parseISO(event.expires_at), new Date()) @@ -677,6 +767,43 @@ export const GalleryView: React.FC = ({ slug, event }) => { )} + {/* Client Access Banner (#172) */} + {isClient && ( +
+
+
+ + + {t('clientAccess.banner')} + + + {t('clientAccess.visibleCount', { visible: visibleCount, total: totalCount })} + +
+ {isSelectionMode && selectedPhotos.size > 0 && ( +
+ + +
+ )} +
+
+ )} + {/* Search and Filters - Only for grid layout */} {!showSidebar ? (
@@ -739,6 +866,8 @@ export const GalleryView: React.FC = ({ slug, event }) => { heroDividerStyle={data?.event?.hero_divider_style || theme.heroDividerStyle || 'wave'} heroImageAnchor={data?.event?.hero_image_anchor || 'center'} welcomeMessage={event.welcome_message} + isClient={isClient} + onToggleVisibility={isClient ? handleToggleVisibility : undefined} />
diff --git a/frontend/src/components/gallery/PhotoGridWithLayouts.tsx b/frontend/src/components/gallery/PhotoGridWithLayouts.tsx index 3e7667ef..4af96f79 100644 --- a/frontend/src/components/gallery/PhotoGridWithLayouts.tsx +++ b/frontend/src/components/gallery/PhotoGridWithLayouts.tsx @@ -68,6 +68,9 @@ interface PhotoGridWithLayoutsProps { welcomeMessage?: string; // Logout callback for full-page layouts onLogout?: () => void; + // Client visibility controls (#172) + isClient?: boolean; + onToggleVisibility?: (photoId: number, currentVisibility: string) => void; } export const PhotoGridWithLayouts: React.FC = ({ @@ -100,7 +103,9 @@ export const PhotoGridWithLayouts: React.FC = ({ heroDividerStyle = 'wave', heroImageAnchor = 'center', welcomeMessage, - onLogout + onLogout, + isClient = false, + onToggleVisibility }) => { const { t } = useTranslation(); const { theme } = useTheme(); @@ -231,6 +236,8 @@ export const PhotoGridWithLayouts: React.FC = ({ heroLogoPosition, welcomeMessage, onLogout, + isClient, + onToggleVisibility, }; // Determine if we should show hero header (decoupled from layout) diff --git a/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx b/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx index 913cc5c9..2326dd9f 100644 --- a/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx @@ -33,6 +33,9 @@ export interface BaseGalleryLayoutProps { }; // Logout callback for full-page layouts onLogout?: () => void; + // Client visibility controls (#172) + isClient?: boolean; + onToggleVisibility?: (photoId: number, currentVisibility: string) => void; } export abstract class BaseGalleryLayout extends React.Component { diff --git a/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx b/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx index a22c6d61..c3236355 100644 --- a/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Download, Maximize2, Check, MessageSquare, Star, Heart, Video } from 'lucide-react'; +import { Download, Maximize2, Check, MessageSquare, Star, Heart, Video, Eye, EyeOff } from 'lucide-react'; import { useInView } from 'react-intersection-observer'; import { useTranslation } from 'react-i18next'; import { useTheme } from '../../../contexts/ThemeContext'; @@ -365,7 +365,9 @@ export const GridGalleryLayout: React.FC = ({ useEnhancedProtection = false, useCanvasRendering = false, feedbackEnabled = false, - feedbackOptions + feedbackOptions, + isClient = false, + onToggleVisibility }) => { const { theme } = useTheme(); const gallerySettings = theme.gallerySettings || {}; @@ -388,40 +390,61 @@ export const GridGalleryLayout: React.FC = ({ return (
- {photos.map((photo, index) => ( - onPhotoClick(index)} - onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo.id)} - onDownload={(e) => onDownload(photo, e)} - animationType={animation} - allowDownloads={allowDownloads} - slug={slug} - protectionLevel={protectionLevel} - useEnhancedProtection={useEnhancedProtection} - useCanvasRendering={useCanvasRendering} - feedbackEnabled={feedbackEnabled} - feedbackOptions={feedbackOptions} - savedIdentity={savedIdentity} - onRequireIdentity={(action, photoId) => { - setPendingAction({ type: action, photoId }); - setShowIdentityModal(true); - }} - onQuickComment={() => onOpenPhotoWithFeedback && onOpenPhotoWithFeedback(index)} - onFeedbackChange={onFeedbackChange} - liked={likedPhotoIds.has(photo.id)} - onLikeSuccess={() => { - setLikedPhotoIds((prev) => { - const next = new Set(prev); - next.add(photo.id); - return next; - }); - }} - /> - ))} + {photos.map((photo, index) => { + const isHidden = photo.visibility === 'hidden'; + return ( +
+ onPhotoClick(index)} + onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo.id)} + onDownload={(e) => onDownload(photo, e)} + animationType={animation} + allowDownloads={allowDownloads} + slug={slug} + protectionLevel={protectionLevel} + useEnhancedProtection={useEnhancedProtection} + useCanvasRendering={useCanvasRendering} + feedbackEnabled={feedbackEnabled} + feedbackOptions={feedbackOptions} + savedIdentity={savedIdentity} + onRequireIdentity={(action, photoId) => { + setPendingAction({ type: action, photoId }); + setShowIdentityModal(true); + }} + onQuickComment={() => onOpenPhotoWithFeedback && onOpenPhotoWithFeedback(index)} + onFeedbackChange={onFeedbackChange} + liked={likedPhotoIds.has(photo.id)} + onLikeSuccess={() => { + setLikedPhotoIds((prev) => { + const next = new Set(prev); + next.add(photo.id); + return next; + }); + }} + /> + {/* Client visibility toggle overlay (#172) */} + {isClient && onToggleVisibility && ( + + )} +
+ ); + })} { setShowIdentityModal(false); setPendingAction(null); }} diff --git a/frontend/src/contexts/GalleryAuthContext.tsx b/frontend/src/contexts/GalleryAuthContext.tsx index 85acfae5..8b70da34 100644 --- a/frontend/src/contexts/GalleryAuthContext.tsx +++ b/frontend/src/contexts/GalleryAuthContext.tsx @@ -11,6 +11,7 @@ import { setActiveGallerySlug, storeGalleryToken, } from '../utils/galleryAuthStorage'; +import type { GalleryAccessLevel } from '../types'; interface GalleryEvent { id: number; @@ -37,7 +38,10 @@ const normalizeEvent = (incoming: GalleryEvent | null | undefined): GalleryEvent interface GalleryAuthContextType { isAuthenticated: boolean; event: GalleryEvent | null; + accessLevel: GalleryAccessLevel; + isClient: boolean; login: (slug: string, password?: string, recaptchaToken?: string | null) => Promise; + clientLogin: (slug: string, password: string) => Promise; logout: () => void; isLoading: boolean; error: string | null; @@ -60,6 +64,7 @@ interface GalleryAuthProviderProps { export const GalleryAuthProvider: React.FC = ({ children }) => { const [isAuthenticated, setIsAuthenticated] = useState(false); const [event, setEvent] = useState(null); + const [accessLevel, setAccessLevel] = useState('guest'); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [routeError, setRouteError] = useState(null); @@ -188,6 +193,14 @@ export const GalleryAuthProvider: React.FC = ({ childr } } + // Restore access level from session storage + const storedAccessLevel = sessionStorage.getItem(`gallery_access_level_${currentSlug}`); + if (storedAccessLevel === 'client') { + setAccessLevel('client'); + } else { + setAccessLevel('guest'); + } + const initialise = async () => { try { setIsLoading(true); @@ -285,15 +298,43 @@ export const GalleryAuthProvider: React.FC = ({ childr } }; + const clientLoginFn = async (slug: string, password: string) => { + try { + setRouteError(null); + setError(null); + setIsLoading(true); + const response = await authService.clientLogin(slug, password); + if (response.token) { + storeGalleryToken(slug, response.token); + } + setActiveGallerySlug(slug); + const normalizedEvent = normalizeEvent(response.event); + setEvent(normalizedEvent); + if (normalizedEvent) { + sessionStorage.setItem(`gallery_event_${slug}`, JSON.stringify(normalizedEvent)); + } + setAccessLevel(response.accessLevel || 'client'); + sessionStorage.setItem(`gallery_access_level_${slug}`, 'client'); + setIsAuthenticated(true); + } catch (err: any) { + setError(err.response?.data?.error || 'Invalid PIN'); + throw err; + } finally { + setIsLoading(false); + } + }; + const logout = () => { const currentSlug = routeInfo.slug; if (currentSlug) { sessionStorage.removeItem(`gallery_event_${currentSlug}`); + sessionStorage.removeItem(`gallery_access_level_${currentSlug}`); clearGalleryToken(currentSlug); } authService.galleryLogout(currentSlug || undefined); setIsAuthenticated(false); setEvent(null); + setAccessLevel('guest'); clearActiveGallerySlug(); }; @@ -302,7 +343,10 @@ export const GalleryAuthProvider: React.FC = ({ childr value={{ isAuthenticated, event, + accessLevel, + isClient: accessLevel === 'client', login, + clientLogin: clientLoginFn, logout, isLoading, error: routeError ?? error, diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 31d0f5b2..0cc825d8 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -1758,6 +1758,13 @@ "username": "Benutzernamen wählen", "password": "Passwort erstellen", "submit": "Konto erstellen" + }, + "photos": { + "hidden": "Versteckt", + "hideSelected": "Ausblenden", + "showSelected": "Einblenden", + "hiddenSuccess": "Fotos für Gäste ausgeblendet", + "visibleSuccess": "Fotos jetzt für Gäste sichtbar" } }, "permissions": { @@ -2576,6 +2583,31 @@ "exportFiltered": "Gefilterte Fotos exportieren", "hint": "Fotos auswählen oder Filter anwenden zum Exportieren" }, + "clientAccess": { + "title": "Kundenzugang", + "description": "Geben Sie Ihre PIN ein, um Fotos vor der Veröffentlichung zu überprüfen und auszuwählen.", + "pinLabel": "Kunden-PIN", + "pinPlaceholder": "PIN eingeben", + "enterPin": "Geben Sie die PIN ein, die Sie erhalten haben", + "invalidPin": "Ungültige PIN. Bitte versuchen Sie es erneut.", + "loginFailed": "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut.", + "loginButton": "Zugang erhalten", + "guestHint": "Nur Fotos ansehen?", + "guestLink": "Zur Gästegalerie", + "banner": "Kundenmodus", + "visibleCount": "{{visible}} von {{total}} Fotos für Gäste sichtbar", + "hideSelected": "Ausgewählte ausblenden", + "showSelected": "Ausgewählte einblenden", + "adminTitle": "Kundenzugang", + "enableToggle": "Kundenzugang aktivieren", + "enableDescription": "Ermöglichen Sie Kunden, Fotos zu überprüfen und auszublenden, bevor die Galerie mit den Gästen geteilt wird.", + "pinHelperText": "Der Kunde verwendet diese PIN, um die Überprüfungsseite aufzurufen.", + "pinUpdated": "Kunden-PIN aktualisiert", + "setPin": "PIN festlegen", + "linkLabel": "Kundenzugangs-Link", + "regenerateToken": "Link neu generieren", + "tokenRegenerated": "Kundenzugangs-Link neu generiert" + }, "adminLogin": { "title": "Admin-Anmeldung", "subtitle": "Melden Sie sich an, um Ihre Fotogalerien zu verwalten", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 29911f9a..5f001149 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1471,6 +1471,13 @@ "username": "Choose a Username", "password": "Create Password", "submit": "Create Account" + }, + "photos": { + "hidden": "Hidden", + "hideSelected": "Hide", + "showSelected": "Show", + "hiddenSuccess": "Photos hidden from guests", + "visibleSuccess": "Photos now visible to guests" } }, "permissions": { @@ -2337,5 +2344,30 @@ "needHelp": "Need help? Contact", "poweredBy": "Powered by PicPeak", "devModeHint": "Development Mode: Use email: admin@example.com, password: admin123" + }, + "clientAccess": { + "title": "Client Access", + "description": "Review photos and manage visibility before the gallery is shared with guests.", + "pinLabel": "Client PIN", + "pinPlaceholder": "Enter your client PIN", + "enterPin": "Please enter your client PIN", + "invalidPin": "Invalid PIN. Please try again.", + "loginFailed": "Authentication failed. Please try again.", + "loginButton": "Access Gallery", + "guestHint": "Looking for the guest gallery?", + "guestLink": "Go to guest view", + "banner": "Client Mode", + "visibleCount": "{{visible}} of {{total}} photos visible to guests", + "hideSelected": "Hide Selected", + "showSelected": "Show Selected", + "adminTitle": "Client Access", + "enableToggle": "Enable Client Access", + "enableDescription": "Allow clients to review and hide photos before the gallery is shared with guests.", + "pinHelperText": "The client will use this PIN to access the review page.", + "pinUpdated": "Client PIN updated", + "setPin": "Set PIN", + "linkLabel": "Client Access Link", + "regenerateToken": "Regenerate link", + "tokenRegenerated": "Client access link regenerated" } } diff --git a/frontend/src/i18n/locales/pt.json b/frontend/src/i18n/locales/pt.json index 9303f58f..b37991aa 100644 --- a/frontend/src/i18n/locales/pt.json +++ b/frontend/src/i18n/locales/pt.json @@ -1,72 +1,72 @@ { "userManagement": { - "title": "Gerenciamento de Usuarios", - "subtitle": "Gerenciar usuarios administradores e convites", - "loading": "Carregando usuarios...", - "loadError": "Falha ao carregar usuarios. Por favor, tente novamente.", - "inviteUser": "Convidar Usuario", + "title": "Gerenciamento de Usuários", + "subtitle": "Gerencie usuários administradores e convites", + "loading": "Carregando usuários...", + "loadError": "Falha ao carregar usuários. Por favor, tente novamente.", + "inviteUser": "Convidar Usuário", "createInvitation": "Criar Convite", - "email": "Endereco de Email", + "email": "Endereço de email", "emailPlaceholder": "usuario@exemplo.com", - "role": "Funcao", - "selectRole": "Selecione uma funcao", + "role": "Função", + "selectRole": "Selecione uma função", "sendInvitation": "Enviar Convite", - "editUser": "Editar Usuario", - "editingUser": "Editando usuario", - "saveChanges": "Salvar Alteracoes", - "deactivateUser": "Desativar Usuario", + "editUser": "Editar Usuário", + "editingUser": "Editando usuário", + "saveChanges": "Salvar Alterações", + "deactivateUser": "Desativar Usuário", "cancelInvitation": "Cancelar Convite", "invitationSent": "Convite enviado com sucesso", "invitationError": "Falha ao enviar convite", "invitationCancelled": "Convite cancelado", "cancelInvitationError": "Falha ao cancelar convite", - "userUpdated": "Usuario atualizado com sucesso", - "updateUserError": "Falha ao atualizar usuario", - "userDeactivated": "Usuario desativado com sucesso", - "deactivateUserError": "Falha ao desativar usuario", - "noRole": "Sem Funcao", + "userUpdated": "Usuário atualizado com sucesso", + "updateUserError": "Falha ao atualizar usuário", + "userDeactivated": "Usuário desativado com sucesso", + "deactivateUserError": "Falha ao desativar usuário", + "noRole": "Sem Função", "neverLoggedIn": "Nunca fez login", "expired": "Expirado", "deactivate": "Desativar", "cancel": "Cancelar", "tabs": { - "users": "Usuarios", + "users": "Usuários", "invitations": "Convites" }, "stats": { - "totalUsers": "Total de Usuarios", - "activeUsers": "Usuarios Ativos", + "totalUsers": "Total de Usuários", + "activeUsers": "Usuários Ativos", "pendingInvitations": "Convites Pendentes", - "inactiveUsers": "Usuarios Inativos" + "inactiveUsers": "Usuários Inativos" }, "status": { "active": "Ativo", "inactive": "Inativo" }, "table": { - "user": "Usuario", + "user": "Usuário", "email": "Email", - "role": "Funcao", + "role": "Função", "status": "Status", - "lastLogin": "Ultimo Login", - "actions": "Acoes", + "lastLogin": "Último Login", + "actions": "Ações", "invitedBy": "Convidado Por", "expires": "Expira" }, "validation": { - "emailRequired": "Email e obrigatorio", - "emailInvalid": "Formato de email invalido", - "roleRequired": "Funcao e obrigatoria" + "emailRequired": "Email é obrigatório", + "emailInvalid": "Formato de email inválido", + "roleRequired": "Função é obrigatória" }, - "searchUsersPlaceholder": "Buscar usuarios...", + "searchUsersPlaceholder": "Buscar usuários...", "searchInvitationsPlaceholder": "Buscar convites...", - "noUsers": "Nenhum usuario encontrado", - "noUsersFound": "Nenhum usuario corresponde a sua busca", + "noUsers": "Nenhum usuário encontrado", + "noUsersFound": "Nenhum usuário corresponde à sua busca", "noInvitations": "Nenhum convite pendente", - "noInvitationsFound": "Nenhum convite corresponde a sua busca", + "noInvitationsFound": "Nenhum convite corresponde à sua busca", "confirmDeactivate": { - "title": "Desativar Usuario", - "message": "Tem certeza que deseja desativar {{name}}? Ele(a) nao podera mais fazer login." + "title": "Desativar Usuário", + "message": "Tem certeza que deseja desativar {{name}}? Ele(a) não poderá mais fazer login." }, "confirmCancelInvitation": { "title": "Cancelar Convite", @@ -85,9 +85,9 @@ "filter": "Filtrar", "sortBy": "Ordenar por", "yes": "Sim", - "no": "Nao", + "no": "Não", "back": "Voltar", - "next": "Proximo", + "next": "Próximo", "previous": "Anterior", "close": "Fechar", "logout": "Sair", @@ -100,22 +100,22 @@ "uploaded": "Enviado", "photo": "foto", "photos": "fotos", - "video": "video", - "videos": "videos", - "media": "midia", + "video": "vídeo", + "videos": "vídeos", + "media": "mídia", "restore": "Restaurar", - "actions": "Acoes", + "actions": "Ações", "refresh": "Atualizar", - "preview": "Visualizar", + "preview": "Pré-visualização", "processing": "Processando...", "upload": "Enviar", "days": "dias", "customize": "Personalizar", "hide": "Ocultar", "unknown": "Desconhecido", - "notSet": "Nao definido", + "notSet": "Não definido", "of": "de", - "up": "Acima", + "up": "Cima", "select": "Selecionar", "selected": "Selecionado", "chunk": "Lote", @@ -124,56 +124,56 @@ "upload": { "photoCategory": "Categoria da Foto", "noCategory": "Sem categoria", - "eventSpecific": "(Especifico do evento)", + "eventSpecific": "(Específico do evento)", "clickToUpload": "Clique para enviar ou arraste e solte", - "fileRequirements": "JPEG, PNG ou WebP (max 50MB por arquivo, {{limit}} arquivos por envio)", - "fileRequirementsMedia": "Imagens JPEG, PNG ou WebP, mais videos MP4/MOV/WEBM (max 50MB por arquivo, {{limit}} arquivos por envio)", - "unsupportedFiles": "Alguns arquivos foram ignorados porque o formato nao e suportado (use JPEG/PNG/WebP/MP4/MOV/WEBM).", + "fileRequirements": "JPEG, PNG ou WebP (máx. 50MB por arquivo, {{limit}} arquivos por envio)", + "fileRequirementsMedia": "Imagens JPEG, PNG ou WebP, além de vídeos MP4/MOV/WEBM (máx. 50MB por arquivo, {{limit}} arquivos por envio)", + "unsupportedFiles": "Alguns arquivos foram ignorados porque o formato não é compatível (use JPEG/PNG/WebP/MP4/MOV/WEBM).", "selectedFiles": "Arquivos selecionados", "uploading": "Enviando...", - "uploadComplete": "Envio concluido!", + "uploadComplete": "Envio concluído!", "uploadFailed": "Falha no envio", "someFilesFailed": "Alguns arquivos falharam ao enviar", "uploadPhotos": "Enviar Fotos", - "uploadMedia": "Enviar Fotos e Videos", + "uploadMedia": "Enviar Fotos e Vídeos", "importExternal": "Importar de Pasta Externa", - "externalImportInfo": "Todas as fotos da pasta selecionada serao importadas.", - "selectExternalFolder": "Selecione pasta externa em /external-media", + "externalImportInfo": "Todas as imagens da pasta selecionada serão importadas.", + "selectExternalFolder": "Selecionar pasta externa em /external-media", "importFromSelectedFolder": "Importar da pasta selecionada", - "maxFilesReached": "Maximo de {{limit}} arquivos permitidos", - "someFilesSkipped": "Apenas mais {{allowed}} arquivos podem ser adicionados (limite {{limit}})", - "tooManyFiles": "Maximo de {{limit}} arquivos podem ser enviados de uma vez", + "maxFilesReached": "Máximo de {{limit}} arquivos permitido", + "someFilesSkipped": "Apenas {{allowed}} arquivo(s) pode(m) ser adicionado(s) (limite {{limit}})", + "tooManyFiles": "Máximo de {{limit}} arquivos podem ser enviados de uma vez", "limitInfo": "{{selected}} de {{limit}} arquivos selecionados ({{remaining}} restantes)", "limitReached": "Limite de envio atingido ({{limit}} arquivos por lote)", "uploadingChunks": "Enviando {{count}} arquivos em {{total}} lotes...", - "mediaCategory": "Categoria de midia", + "mediaCategory": "Categoria de mídia", "uploadAction": "Enviar {{count}} arquivos" }, "navigation": { "dashboard": "Painel", "events": "Eventos", "archives": "Arquivos", - "settings": "Configuracoes", + "settings": "Configurações", "eventTypes": "Tipos de Evento", - "branding": "Marca", - "analytics": "Analiticos", - "emailSettings": "Config. de Email", - "backup": "Backup e Restauracao", - "cmsPages": "Paginas CMS", - "users": "Usuarios" + "branding": "Identidade Visual", + "analytics": "Análises", + "emailSettings": "Configurações de Email", + "backup": "Backup e Restauração", + "cmsPages": "Páginas CMS", + "users": "Usuários" }, "archives": { "title": "Arquivos", - "subtitle": "Gerenciar galerias de fotos arquivadas", + "subtitle": "Gerencie galerias de fotos arquivadas", "loadingArchives": "Carregando arquivos...", "totalArchives": "Total de Arquivos", "storageUsed": "Armazenamento Usado", "totalPhotos": "Total de Fotos", - "avgArchiveSize": "Tamanho Medio do Arquivo", + "avgArchiveSize": "Tamanho Médio do Arquivo", "searchPlaceholder": "Buscar arquivos...", "allTypes": "Todos os Tipos", "wedding": "Casamento", - "birthday": "Aniversario", + "birthday": "Aniversário", "corporate": "Corporativo", "other": "Outro", "sortByDate": "Ordenar por Data", @@ -182,10 +182,10 @@ "tableHeaders": { "event": "Evento", "type": "Tipo", - "archivedDate": "Data de Arquivamento", + "archivedDate": "Data de Arquivo", "size": "Tamanho", "photos": "Fotos", - "actions": "Acoes" + "actions": "Ações" }, "noArchivesFound": "Nenhum arquivo encontrado", "eventDateNA": "Data do evento: N/D", @@ -193,26 +193,26 @@ "download": "Baixar", "restore": "Restaurar", "delete": "Excluir", - "showing": "Mostrando {{from}} a {{to}} de {{total}} arquivos", - "page": "Pagina {{current}} de {{total}}", + "showing": "Exibindo {{from}} a {{to}} de {{total}} arquivos", + "page": "Página {{current}} de {{total}}", "storageManagement": "Gerenciamento de Armazenamento", - "storageInfo": "Arquivos sao armazenados permanentemente a menos que sejam excluidos manualmente. Considere implementar uma politica de retencao para gerenciar custos de armazenamento.", - "confirmRestore": "Tem certeza que deseja restaurar este arquivo? O evento ficara ativo novamente.", - "confirmDelete": "Tem certeza que deseja excluir permanentemente este arquivo? Esta acao nao pode ser desfeita.", + "storageInfo": "Os arquivos são armazenados permanentemente, a menos que sejam excluídos manualmente. Considere implementar uma política de retenção para gerenciar os custos de armazenamento.", + "confirmRestore": "Tem certeza que deseja restaurar este arquivo? O evento ficará ativo novamente.", + "confirmDelete": "Tem certeza que deseja excluir permanentemente este arquivo? Esta ação não pode ser desfeita.", "restoreSuccess": "Arquivo restaurado com sucesso", - "deleteSuccess": "Arquivo excluido permanentemente" + "deleteSuccess": "Arquivo excluído permanentemente" }, "auth": { "login": "Entrar", "password": "Senha", "enterPassword": "Digite a Senha da Galeria", "passwordPlaceholder": "Digite a senha da galeria", - "invalidPassword": "Senha invalida", + "invalidPassword": "Senha inválida", "wrongPassword": "Senha incorreta. Por favor, verifique sua senha e tente novamente.", - "tooManyAttempts": "Muitas tentativas de login. Por favor, tente novamente mais tarde.", - "sessionExpired": "Sessao expirada", + "tooManyAttempts": "Muitas tentativas de login malsucedidas. Por favor, tente novamente mais tarde.", + "sessionExpired": "Sessão expirada", "pleaseEnterPassword": "Por favor, digite uma senha", - "passwordHint": "A senha foi fornecida pelo organizador do evento. Entre em contato se voce nao a possui." + "passwordHint": "A senha foi fornecida pelo organizador do evento. Entre em contato com ele se você não a tiver." }, "gallery": { "title": "Galeria de Fotos", @@ -224,7 +224,7 @@ "dayRemaining": "1 dia restante", "hoursRemaining": "{{hours}} horas restantes", "expiredMessage": "Esta galeria expirou em {{date}}", - "contactOrganizer": "Por favor, entre em contato com o organizador do evento se precisar de acesso a estas fotos.", + "contactOrganizer": "Por favor, entre em contato com o organizador do evento se precisar de acesso a essas fotos.", "searchPhotos": "Buscar fotos por nome de arquivo...", "sortByDate": "Ordenar por Data", "sortByName": "Ordenar por Nome", @@ -232,11 +232,11 @@ "allPhotos": "Todas as Fotos", "filter": "Filtrar", "feedbackFilter": "Filtro de Feedback", - "all": "Todos", - "liked": "Curtidos", - "favorited": "Favoritos", + "all": "Todas", + "liked": "Curtidas", + "favorited": "Favoritadas", "favorites": "Favoritos", - "downloadSelected": "Baixar {{count}} Selecionados", + "downloadSelected": "Baixar {{count}} Selecionadas", "shareGallery": "Compartilhar Galeria", "needHelp": "Precisa de ajuda? Entre em contato em", "noPhotosFound": "Nenhuma foto encontrada", @@ -246,8 +246,8 @@ "expiredOn": "Esta galeria expirou em {{date}}.", "expiresIn": "A galeria expira em {{count}} dia", "expiresIn_plural": "A galeria expira em {{count}} dias", - "downloadBefore": "Baixe suas fotos antes que nao estejam mais disponiveis.", - "publicGalleryTitle": "Esta galeria e acessivel publicamente", + "downloadBefore": "Baixe suas fotos antes que elas não estejam mais disponíveis.", + "publicGalleryTitle": "Esta galeria é de acesso público", "publicGallerySubtitle": "Carregando as fotos agora...", "viewGallery": "Ver Galeria", "downloadAll": "Baixar Tudo", @@ -257,114 +257,114 @@ "downloadedPhotos_plural": "{{count}} fotos baixadas!", "downloadError": "Algumas fotos falharam ao baixar", "selectPhotos": "Selecionar Fotos", - "cancelSelection": "Cancelar Selecao", - "photosSelected": "{{count}} selecionado(s)", + "cancelSelection": "Cancelar Seleção", + "photosSelected": "{{count}} selecionadas", "selectAll": "Selecionar Tudo", "deselectAll": "Desmarcar Tudo", - "deleteSelected": "Excluir Selecionados", + "deleteSelected": "Excluir Selecionadas", "photosCount": "{{count}} foto", "photosCount_plural": "{{count}} fotos", "searchByFilename": "Buscar por nome de arquivo...", "uncategorized": "Sem categoria", - "sortAscending": "Ordem crescente", - "sortDescending": "Ordem decrescente", - "remaining": "restante(s)", - "selectPhotosHint": "Dica: Use Ctrl+Clique (Cmd+Clique no Mac) para selecionar varias fotos rapidamente", + "sortAscending": "Ordenar crescente", + "sortDescending": "Ordenar decrescente", + "remaining": "restantes", + "selectPhotosHint": "Dica: Use Ctrl+Clique (Cmd+Clique no Mac) para selecionar várias fotos rapidamente", "filters": "Filtros", "openFilters": "Abrir filtros", "toggleSidebar": "Alternar barra lateral", "toggleMenu": "Alternar menu", "allCategories": "Todas as Categorias", "categories": "Categorias", - "mediaType": "Midia", - "allMedia": "Toda midia", + "mediaType": "Mídia", + "allMedia": "Toda a mídia", "photosOnly": "Fotos", - "videosOnly": "Videos", + "videosOnly": "Vídeos", "download": "Baixar", - "noMedia": "Nenhuma midia enviada ainda", + "noMedia": "Nenhuma mídia enviada ainda", "searchPlaceholder": "Buscar fotos...", "sortBy": "Ordenar Por", "sortByCaptureDate": "Ordenar por Data de Captura", - "sortByRating": "Ordenar por Avaliacao", + "sortByRating": "Ordenar por Avaliação", "photoGallery": "Galeria de Fotos", "photos": "Fotos", "allRightsReserved": "Todos os direitos reservados", - "searchMemories": "Buscar memorias...", + "searchMemories": "Buscar memórias...", "thankYou": "Obrigado", - "thankYouMessage": "Por fazer parte da nossa historia e tornar nosso dia especial inesquecivel.", + "thankYouMessage": "Por fazer parte da nossa história e tornar nosso dia especial inesquecível.", "feedback": { "title": "Feedback", - "shareThoughts": "Compartilhe seus pensamentos sobre \"{{name}}\"", + "shareThoughts": "Compartilhe sua opinião sobre \"{{name}}\"", "rateThisMoment": "Avalie este momento", - "comments": "Comentarios", - "noComments": "Nenhum comentario ainda. Seja o primeiro!", + "comments": "Comentários", + "noComments": "Nenhum comentário ainda. Seja o primeiro!", "yourName": "Seu nome", "yourEmail": "Seu email", "writeLovelyNote": "Escreva uma mensagem carinhosa...", - "postComment": "Publicar Comentario", - "anonymous": "Anonimo" + "postComment": "Publicar Comentário", + "anonymous": "Anônimo" } }, "categories": { "title": "Categorias de Fotos", "global": "Categorias Globais", - "eventSpecific": "Categorias do Evento", + "eventSpecific": "Categorias Específicas do Evento", "addCategory": "Adicionar Categoria", - "organizationInfo": "Organize suas fotos em categorias. Categorias ajudam os convidados a navegar e encontrar tipos especificos de fotos.", - "eventSpecificCategories": "Categorias Especificas do Evento", - "noEventSpecificCategories": "Nenhuma categoria especifica do evento. Categorias globais estao disponiveis por padrao.", - "globalCategoriesAlwaysAvailable": "Categorias Globais (sempre disponiveis):", + "organizationInfo": "Organize suas fotos em categorias. As categorias ajudam os convidados a navegar e encontrar tipos específicos de fotos.", + "eventSpecificCategories": "Categorias Específicas do Evento", + "noEventSpecificCategories": "Sem categorias específicas do evento. As categorias globais estão disponíveis por padrão.", + "globalCategoriesAlwaysAvailable": "Categorias Globais (sempre disponíveis):", "deleteCategoryTitle": "Excluir categoria", "categoryCreatedSuccess": "Categoria criada com sucesso", - "categoryDeletedSuccess": "Categoria excluida com sucesso", + "categoryDeletedSuccess": "Categoria excluída com sucesso", "failedToCreateCategory": "Falha ao criar categoria", "failedToDeleteCategory": "Falha ao excluir categoria", "categoryName": "Nome da categoria", "noCategory": "Sem categoria", - "noCategoriesYet": "Nenhuma categoria ainda. Crie sua primeira categoria para organizar fotos.", - "deleteConfirm": "Tem certeza que deseja excluir \"{{name}}\"?", - "cannotDelete": "Nao e possivel excluir categoria com fotos. Por favor, reatribua as fotos primeiro.", + "noCategoriesYet": "Nenhuma categoria ainda. Crie sua primeira categoria para organizar as fotos.", + "deleteConfirm": "Tem certeza de que deseja excluir \"{{name}}\"?", + "cannotDelete": "Não é possível excluir uma categoria com fotos. Reatribua as fotos primeiro.", "setCoverPhoto": "Definir Foto de Capa", "removeCoverPhoto": "Remover Foto de Capa", "coverPhotoSet": "Foto de capa definida com sucesso", "coverPhotoRemoved": "Foto de capa removida", "failedToSetCoverPhoto": "Falha ao definir foto de capa", - "categoryHeroHint": "Se nenhuma foto de capa for definida para uma categoria, a foto hero padrao sera usada." + "categoryHeroHint": "Se nenhuma foto de capa for definida para uma categoria, a foto hero padrão será utilizada." }, "events": { "title": "Eventos", "create": "Criar", "createEvent": "Criar Evento", - "totalViews": "Total de Visualizacoes", + "totalViews": "Total de Visualizações", "totalDownloads": "Total de Downloads", - "uniqueVisitors": "Visitantes Unicos", + "uniqueVisitors": "Visitantes Únicos", "createNewEvent": "Criar Novo Evento", "setupNewGallery": "Configure uma nova galeria de fotos para seu evento", "createNewEventSubtitle": "Configure uma nova galeria de fotos para seu evento", - "eventNamePlaceholder": "ex., Casamento do Joao e Maria", - "welcomeMessageOptional": "Mensagem de Boas-vindas (Opcional)", - "welcomeMessagePlaceholder": "Bem-vindos ao nosso dia especial! Fique a vontade para baixar e compartilhar estas memorias...", + "eventNamePlaceholder": "Ex.: Casamento de João e Maria", + "welcomeMessageOptional": "Mensagem de Boas-Vindas (Opcional)", + "welcomeMessagePlaceholder": "Bem-vindo(a) ao nosso dia especial! Sinta-se à vontade para baixar e compartilhar essas memórias...", "hostEmailPlaceholder": "cliente@exemplo.com", "adminEmailPlaceholder": "admin@exemplo.com", - "securityAndAccess": "Seguranca e Acesso", - "accessAndSecurity": "Acesso e Seguranca", + "securityAndAccess": "Segurança e Acesso", + "accessAndSecurity": "Acesso e Segurança", "enterPassword": "Digite a senha", "passwordPlaceholder": "Digite uma senha segura", "confirmPasswordPlaceholder": "Confirme a senha", - "galleryExpiresOn": "A galeria expira em {{date}}", - "guestsWillReceiveWarning": "Os convidados receberao um email de aviso 7 dias antes da expiracao.", + "galleryExpiresOn": "A galeria expirará em {{date}}", + "guestsWillReceiveWarning": "Os convidados receberão um email de aviso 7 dias antes da expiração.", "types": { "wedding": "Casamento", - "birthday": "Aniversario", + "birthday": "Aniversário", "corporate": "Corporativo", "other": "Outro" }, "themes": { - "default": "Padrao", + "default": "Padrão", "oceanBlue": "Azul Oceano", "royalPurple": "Roxo Real", - "roseGold": "Rose Gold", - "sunsetAmber": "Ambar do Por do Sol" + "roseGold": "Ouro Rosé", + "sunsetAmber": "Âmbar Pôr do Sol" }, "eventDetails": "Detalhes do Evento", "eventName": "Nome do Evento", @@ -372,10 +372,10 @@ "eventDate": "Data do Evento", "hostEmail": "Email do Cliente", "hostName": "Nome do Cliente", - "hostNamePlaceholder": "Joao Silva", - "adminEmail": "Email do Admin", - "adminNotificationEmail": "Email de Notificacao do Admin", - "expirationDate": "Data de Expiracao", + "hostNamePlaceholder": "João Silva", + "adminEmail": "Email do Administrador", + "adminNotificationEmail": "Email de Notificação do Administrador", + "expirationDate": "Data de Expiração", "active": "Ativo", "archived": "Arquivado", "photoCount": "{{count}} fotos", @@ -386,114 +386,114 @@ "viewGallery": "Ver Galeria", "uploadPhotos": "Enviar Fotos", "archiveEvent": "Arquivar Evento", - "archiveConfirm": "Tem certeza que deseja arquivar este evento? Esta acao nao pode ser desfeita.", + "archiveConfirm": "Tem certeza de que deseja arquivar este evento? Esta ação não pode ser desfeita.", "extendExpiration": "Estender {{days}} Dias", "backToEvents": "Voltar aos Eventos", "loadingEventDetails": "Carregando detalhes do evento...", - "saveChanges": "Salvar Alteracoes", + "saveChanges": "Salvar Alterações", "eventExpired": "Este evento expirou", "eventExpiresIn": "Este evento expira em {{days}} dias", - "guestsNoAccess": "Os convidados nao podem mais acessar a galeria. Considere arquivar este evento.", + "guestsNoAccess": "Os convidados não podem mais acessar a galeria. Considere arquivar este evento.", "warningEmailsSent": "Emails de aviso foram enviados ao cliente.", - "overview": "Visao Geral", + "overview": "Visão Geral", "photos": "Fotos", "categories": "Categorias", - "eventInformation": "Informacoes do Evento", + "eventInformation": "Informações do Evento", "sourceMode": "Modo de Origem", "sourceModeManaged": "Gerenciado (enviar para o PicPeak)", "sourceModeReference": "Referenciar pasta externa", "sourceModeHelp": "Use o modo gerenciado para uploads diretos ou aponte para uma pasta /external-media montada ao usar armazenamento local.", "externalFolder": "Pasta Externa", - "externalFolderHint": "Estas pastas sao lidas da montagem /external-media dentro do seu container ou host.", - "externalFolderRequired": "Por favor, selecione uma pasta externa antes de salvar.", - "welcomeMessage": "Mensagem de Boas-vindas", + "externalFolderHint": "Essas pastas são lidas do ponto de montagem /external-media dentro do seu container ou host.", + "externalFolderRequired": "Selecione uma pasta externa antes de salvar.", + "welcomeMessage": "Mensagem de Boas-Vindas", "noWelcomeMessage": "Nenhuma mensagem de boas-vindas definida", "created": "Criado", "expires": "Expira", - "shareWithGuests": "Compartilhe este link com os convidados. Eles precisarao da senha para acessar a galeria.", - "shareWithGuestsPublic": "Compartilhe este link com os convidados. Nenhuma senha e necessaria para esta galeria.", + "shareWithGuests": "Compartilhe este link com os convidados. Eles precisarão da senha para acessar a galeria.", + "shareWithGuestsPublic": "Compartilhe este link com os convidados. Nenhuma senha é necessária para esta galeria.", "resetGalleryPassword": "Redefinir Senha da Galeria", - "resendCreationEmail": "Reenviar Email de Criacao", - "creationEmailResent": "Email de criacao foi enfileirado para envio", - "failedToResendEmail": "Falha ao reenviar email de criacao", - "photoStatistics": "Estatisticas de Fotos", + "resendCreationEmail": "Reenviar Email de Criação", + "creationEmailResent": "Email de criação foi colocado na fila para envio", + "failedToResendEmail": "Falha ao reenviar email de criação", + "photoStatistics": "Estatísticas de Fotos", "totalPhotos": "Total de Fotos", "managePhotos": "Gerenciar Fotos", - "actions": "Acoes", - "archivingInfo": "O arquivamento criara um arquivo ZIP de todas as fotos e removera a galeria do acesso publico.", - "statistics": "Estatisticas", - "views": "Visualizacoes", + "actions": "Ações", + "archivingInfo": "O arquivamento criará um arquivo ZIP de todas as fotos e removerá a galeria do acesso público.", + "statistics": "Estatísticas", + "views": "Visualizações", "downloads": "Downloads", - "noStatistics": "Nenhuma estatistica disponivel ainda", + "noStatistics": "Nenhuma estatística disponível ainda", "archiveStatus": "Status do Arquivo", "archivedOn": "Arquivado Em", "downloadArchive": "Baixar Arquivo", "loadingPhotos": "Carregando fotos...", "photoCategories": "Categorias de Fotos", - "organizeCategoriesInfo": "Organize suas fotos em categorias. Categorias ajudam os convidados a navegar e encontrar tipos especificos de fotos.", - "categoriesTip": "Dica: Categorias sao especificas para cada evento. Voce tambem pode criar categorias globais em Configuracoes.", - "contactInformation": "Informacoes de Contato", - "hostEmailHelp": "O cliente recebera notificacoes de criacao e expiracao da galeria", - "adminEmailHelp": "Recebera notificacoes do sistema e confirmacoes de arquivamento", - "securityAccess": "Seguranca e Acesso", + "organizeCategoriesInfo": "Organize suas fotos em categorias. As categorias ajudam os convidados a navegar e encontrar tipos específicos de fotos.", + "categoriesTip": "Dica: As categorias são específicas para cada evento. Você pode criar categorias personalizadas como \"Cerimônia\", \"Recepção\", \"Retratos\", etc.", + "contactInformation": "Informações de Contato", + "hostEmailHelp": "O cliente receberá notificações de criação e expiração da galeria", + "adminEmailHelp": "Receberá notificações do sistema e confirmações de arquivamento", + "securityAccess": "Segurança e Acesso", "galleryPassword": "Senha da Galeria", "requirePasswordToggle": "Exigir senha para esta galeria", - "requirePasswordToggleHelp": "Desative isto se quiser compartilhar a galeria sem senha. Qualquer pessoa com o link podera ver as fotos.", - "publicGalleryWarning": "Galerias publicas sao acessiveis a qualquer pessoa com o link. Considere ativar marcas d'agua nos downloads e monitorar a atividade.", - "passwordHelperText": "Voce pode usar datas como \"04.07.2025\" ou qualquer texto com 6+ caracteres", + "requirePasswordToggleHelp": "Desative isso se quiser compartilhar a galeria sem senha. Qualquer pessoa com o link poderá ver as fotos.", + "publicGalleryWarning": "Galerias públicas são acessíveis a qualquer pessoa com o link. Considere ativar marcas d'água nos downloads e monitorar a atividade.", + "passwordHelperText": "Você pode usar datas como \"04.07.2025\" ou qualquer texto com 6+ caracteres", "confirmPassword": "Confirmar Senha", "showPasswords": "Mostrar senhas", "newPasswordLabel": "Nova Senha da Galeria", - "gallerySettings": "Configuracoes da Galeria", + "gallerySettings": "Configurações da Galeria", "themeAndStyle": "Tema e Estilo", "colorTheme": "Tema de Cores", - "galleryExpiration": "Expiracao da Galeria", - "galleryExpiresIn": "Galeria Expira Em", - "daysAfterEvent": "dias apos a data do evento", + "galleryExpiration": "Expiração da Galeria", + "galleryExpiresIn": "A Galeria Expira Em", + "daysAfterEvent": "dias após a data do evento", "expiresOn": "Expira em", - "galleryWillExpireOn": "A galeria expira em {{date}}", - "expirationWarning": "Os convidados receberao um email de aviso 7 dias antes da expiracao.", - "noExpiration": "Sem Expiracao", - "noExpirationHelp": "Esta galeria permanecera ativa ate ser arquivada manualmente.", + "galleryWillExpireOn": "A galeria expirará em {{date}}", + "expirationWarning": "Os convidados receberão um email de aviso 7 dias antes da expiração.", + "noExpiration": "Sem Expiração", + "noExpirationHelp": "Esta galeria permanecerá ativa até ser arquivada manualmente.", "photoCap": "Limite de Fotos", "photoCapHelp": "Número máximo de fotos permitidas. 0 = ilimitado", - "userUploads": "Configuracoes de Upload do Usuario", + "userUploads": "Configurações de Upload de Convidados", "allowUserUploads": "Permitir que convidados enviem fotos", - "allowUserUploadsHelp": "Permitir que convidados enviem suas proprias fotos para esta galeria", - "allowUserUploadsDescription": "Permitir que convidados enviem suas proprias fotos para esta galeria", + "allowUserUploadsHelp": "Habilitar convidados a enviar suas próprias fotos para esta galeria", + "allowUserUploadsDescription": "Habilitar convidados a enviar suas próprias fotos para esta galeria", "uploadCategory": "Categoria de Upload", - "selectCategory": "Selecione uma categoria para uploads dos usuarios", - "uploadCategoryHelp": "Todas as fotos enviadas pelos usuarios serao adicionadas a esta categoria", - "userUploadWarning": "Uploads dos usuarios serao moderados e podem ser removidos pelos administradores a qualquer momento.", + "selectCategory": "Selecione uma categoria para uploads de convidados", + "uploadCategoryHelp": "Todas as fotos enviadas por convidados serão adicionadas a esta categoria", + "userUploadWarning": "Os uploads de convidados serão moderados e podem ser removidos por administradores a qualquer momento.", "allowDownloads": "Permitir download de fotos", "allowDownloadsHelp": "Permitir que convidados baixem fotos desta galeria", - "downloadPermissions": "Permissoes de Download", + "downloadPermissions": "Permissões de Download", "downloadsEnabled": "Downloads Habilitados", "downloadsDisabled": "Downloads Desabilitados", - "downloadProtection": "Protecao de Download", - "disableRightClick": "Bloquear menu do botao direito", - "watermarkDownloads": "Adicionar marca d'agua aos downloads", - "enableDevtoolsProtection": "Detectar ferramentas de desenvolvedor", - "useCanvasRendering": "Renderizacao canvas (protecao avancada)", - "protectionInfo": "Os recursos de protecao ajudam a prevenir downloads nao autorizados, mas nao podem bloquear todos os metodos.", - "protectionLevelBasic": "Basico - Apenas bloqueio de clique direito", - "protectionLevelStandard": "Padrao - Atalhos de teclado bloqueados", - "protectionLevelEnhanced": "Aprimorado - Deteccao de captura de tela", - "protectionLevelMaximum": "Maximo - Deteccao de DevTools e renderizacao canvas", - "heroLogoSettings": "Configuracoes do Logo Hero", - "heroLogoVisible": "Exibir logo na secao hero", + "downloadProtection": "Proteção de Download", + "disableRightClick": "Bloquear menu de contexto (botão direito)", + "watermarkDownloads": "Adicionar marca d'água aos downloads", + "enableDevtoolsProtection": "Detectar ferramentas do desenvolvedor", + "useCanvasRendering": "Renderização em canvas (proteção avançada)", + "protectionInfo": "Os recursos de proteção ajudam a evitar downloads não autorizados, mas não podem bloquear todos os métodos.", + "protectionLevelBasic": "Básico - Apenas bloqueio de clique direito", + "protectionLevelStandard": "Padrão - Atalhos de teclado bloqueados", + "protectionLevelEnhanced": "Aprimorado - Detecção de print screen", + "protectionLevelMaximum": "Máximo - Detecção de DevTools e renderização em canvas", + "heroLogoSettings": "Configurações do Logo Hero", + "heroLogoVisible": "Exibir logo na seção hero", "heroLogoSize": "Tamanho do Logo", "heroLogoSizeSmall": "Pequeno", - "heroLogoSizeMedium": "Medio", + "heroLogoSizeMedium": "Médio", "heroLogoSizeLarge": "Grande", "heroLogoSizeXLarge": "Extra Grande", - "heroLogoPosition": "Posicao do Logo", - "heroLogoPositionTop": "Topo (acima do titulo)", - "heroLogoPositionCenter": "Centro (entre titulo e datas)", - "heroLogoPositionBottom": "Inferior (abaixo das datas)", - "heroLogoInfo": "Estas configuracoes se aplicam quando a galeria usa o layout Hero. Voce pode ocultar o logo ou personalizar seu tamanho e posicao.", + "heroLogoPosition": "Posição do Logo", + "heroLogoPositionTop": "Topo (acima do título)", + "heroLogoPositionCenter": "Centro (entre o título e as datas)", + "heroLogoPositionBottom": "Rodapé (abaixo das datas)", + "heroLogoInfo": "Essas configurações se aplicam quando a galeria usa o layout Hero. Você pode ocultar o logo ou personalizar seu tamanho e posição.", "eventCustomLogo": "Logo Personalizado do Evento", - "eventCustomLogoDescription": "Envie um logo personalizado para este evento. Isto substitui o logo de marca global apenas para esta galeria.", + "eventCustomLogoDescription": "Envie um logo personalizado para este evento. Isso substitui o logo de identidade visual global somente para esta galeria.", "uploadEventLogo": "Enviar Logo", "replaceLogo": "Substituir", "removeLogo": "Remover", @@ -501,25 +501,25 @@ "eventLogoUploadFailed": "Falha ao enviar logo do evento", "eventLogoRemoved": "Logo do evento removido com sucesso", "eventLogoRemoveFailed": "Falha ao remover logo do evento", - "heroLogoVisibleLabel": "Logo visivel", + "heroLogoVisibleLabel": "Logo visível", "heroLogoSizeLabel": "Tamanho", - "heroLogoPositionLabel": "Posicao", + "heroLogoPositionLabel": "Posição", "heroLogoHidden": "Logo oculto", "heroPhoto": "Foto Hero", - "heroPhotoHelp": "Selecione uma foto hero padrao para a galeria. Voce pode substituir isso por categoria.", + "heroPhotoHelp": "Selecione uma foto hero padrão para a galeria. Você pode substituí-la por categoria.", "selectHeroPhoto": "Selecionar Foto Hero", "noHeroPhotoSelected": "Nenhuma foto hero selecionada", "heroPhotoSelected": "Foto hero selecionada", - "heroImageAnchor": "Posicao de Corte da Imagem Hero", - "heroImageAnchorDescription": "Clique na imagem para definir o ponto focal de corte.", + "heroImageAnchor": "Posição de Corte da Imagem Hero", + "heroImageAnchorDescription": "Clique na imagem para definir o ponto focal do corte.", "heroImageAnchorTop": "Topo", "heroImageAnchorCenter": "Centro", - "heroImageAnchorBottom": "Inferior", - "heroPreview": "Visualizacao hero", - "noPhotosAvailable": "Nenhuma foto disponivel", - "processingRequest": "Processando sua solicitacao...", + "heroImageAnchorBottom": "Rodapé", + "heroPreview": "Pré-visualização Hero", + "noPhotosAvailable": "Nenhuma foto disponível", + "processingRequest": "Processando sua solicitação...", "eventTypeWedding": "Casamento", - "eventTypeBirthday": "Aniversario", + "eventTypeBirthday": "Aniversário", "eventTypeCorporate": "Corporativo", "eventTypeOther": "Outro", "days30": "30 dias", @@ -528,14 +528,14 @@ "days365": "1 ano", "inactive": "Inativo", "expired": "Expirado", - "daysLeft": "{{count}}d restante(s)", - "daysLeft_plural": "{{count}}d restante(s)", + "daysLeft": "({{count}} dia restante)", + "daysLeft_plural": "({{count}} dias restantes)", "subtitle": "Gerencie suas galerias de fotos e eventos", "loadingEvents": "Carregando eventos...", "failedToLoadEvents": "Falha ao carregar eventos", "tryAgain": "Tentar Novamente", "bulkArchiveSuccess": "{{count}} eventos arquivados com sucesso", - "bulkArchivePartial": "{{success}} eventos arquivados, {{failed}} falharam", + "bulkArchivePartial": "{{success}} eventos arquivados, {{failed}} com falha", "searchEventsPlaceholder": "Buscar eventos...", "all": "Todos", "expiring": "Expirando", @@ -543,9 +543,9 @@ "eventsSelected_plural": "{{count}} eventos selecionados", "clear": "Limpar", "archiveSelected": "Arquivar Selecionados", - "publicAccess": "Acesso publico", + "publicAccess": "Acesso público", "passwordProtected": "Protegido por senha", - "newPasswordRequired": "Por favor, defina uma senha antes de ativar a protecao.", + "newPasswordRequired": "Por favor, defina uma senha antes de habilitar a proteção.", "event": "Evento", "type": "Tipo", "date": "Data", @@ -561,320 +561,320 @@ "archiveEventAction": "Arquivar Evento", "downloadArchiveAction": "Baixar Arquivo", "deleteEvent": "Excluir Evento", - "deleteEventConfirm": "Tem certeza que deseja excluir este evento?", - "downloadArchiveSoon": "Download de arquivo em breve", + "deleteEventConfirm": "Tem certeza de que deseja excluir este evento?", + "downloadArchiveSoon": "Download do arquivo em breve", "eventExpiredMessage": "Este evento expirou", - "guestsCannotAccessGallery": "Os convidados nao podem mais acessar a galeria. Considere arquivar este evento.", + "guestsCannotAccessGallery": "Os convidados não podem mais acessar a galeria. Considere arquivar este evento.", "warningEmailsHaveBeenSent": "Emails de aviso foram enviados ao cliente.", "extendSevenDays": "Estender 7 Dias", - "welcomeMessageLabel": "Mensagem de Boas-vindas", + "welcomeMessageLabel": "Mensagem de Boas-Vindas", "noWelcomeMessageSet": "Nenhuma mensagem de boas-vindas definida", "createdOn": "Criado", "copy": "Copiar", "copied": "Copiado!", - "organizingPhotosInfo": "Organize suas fotos em categorias. Categorias ajudam os convidados a navegar e encontrar tipos especificos de fotos.", + "organizingPhotosInfo": "Organize suas fotos em categorias. As categorias ajudam os convidados a navegar e encontrar tipos específicos de fotos.", "archiveStatusTitle": "Status do Arquivo", - "downloadingArchive": "Baixando arquivo {{name}}...", + "downloadingArchive": "Baixando arquivo de {{name}}...", "downloadStarted": "Download iniciado", "failedToDownloadArchive": "Falha ao baixar arquivo", - "statisticsNotAvailable": "Nenhuma estatistica disponivel ainda", + "statisticsNotAvailable": "Nenhuma estatística disponível ainda", "photoFilters": "Filtros de Fotos", - "noStatisticsAvailableYet": "Nenhuma estatistica disponivel ainda", + "noStatisticsAvailableYet": "Nenhuma estatística disponível ainda", "addPlus": "Adicionar+", "galleryTheme": "Tema da Galeria", "customizeTheme": "Personalizar Tema", "noThemeSet": "Nenhum tema configurado", "customizingTheme": "Personalizando tema da galeria", "customizingThemeFor": "Personalizando tema para {{event}}", - "customCssTemplate": "Template CSS Personalizado", - "customCssTemplateDesc": "Aplique um template CSS personalizado para estilizar a galeria com efeitos visuais unicos.", - "noTemplate": "Sem Template", - "useThemeOnly": "Usar apenas predefinicao de tema", - "customTemplate": "Template Personalizado", + "customCssTemplate": "Modelo de CSS Personalizado", + "customCssTemplateDesc": "Aplique um modelo de CSS personalizado para estilizar a galeria com efeitos visuais únicos.", + "noTemplate": "Sem Modelo", + "useThemeOnly": "Usar apenas o tema predefinido", + "customTemplate": "Modelo Personalizado", "rename": { "button": "Renomear", "title": "Renomear Evento", "validating": "Validando novo nome...", "renamingFiles": "Renomeando arquivos...", - "complete": "Concluido!", + "complete": "Concluído!", "failed": "Falha ao renomear", "filesRenamed": "{{count}} arquivos atualizados", "confirm": "Renomear Evento" } }, "settings": { - "title": "Configuracoes do Sistema", - "subtitle": "Configure as configuracoes e preferencias do sistema", - "loadingSettings": "Carregando configuracoes...", + "title": "Configurações do Sistema", + "subtitle": "Defina as configurações e preferências globais do sistema", + "loadingSettings": "Carregando configurações...", "general": { "title": "Geral", - "siteConfiguration": "Configuracao do Site", + "siteConfiguration": "Configuração do Site", "siteUrl": "URL do Site", - "siteUrlHelp": "Usado para gerar links de galeria em emails", - "defaultExpiration": "Expiracao Padrao (dias)", - "defaultExpirationHelp": "Quanto tempo as galerias permanecem ativas por padrao", - "maxFileSize": "Tamanho Maximo do Arquivo (MB)", - "maxFileSizeHelp": "Tamanho maximo por foto enviada", - "maxFilesPerUpload": "Max Arquivos por Upload", - "maxFilesPerUploadHelp": "Numero maximo de fotos permitidas em um unico lote de envio (1-{{max}}).", + "siteUrlHelp": "Usado para gerar links de galeria nos emails", + "defaultExpiration": "Expiração Padrão (dias)", + "defaultExpirationHelp": "Por quanto tempo as galerias permanecem ativas por padrão", + "maxFileSize": "Tamanho Máximo de Arquivo (MB)", + "maxFileSizeHelp": "Tamanho máximo por foto enviada", + "maxFilesPerUpload": "Máximo de Arquivos por Upload", + "maxFilesPerUploadHelp": "Número máximo de fotos permitidas em um único lote de upload (1-{{max}}).", "allowedFileTypes": "Tipos de Arquivo Permitidos", - "allowedFileTypesHelp": "Lista de extensoes de arquivo separadas por virgula", - "featureToggles": "Recursos", - "enableAnalytics": "Ativar rastreamento de analiticos", - "enableRegistration": "Permitir auto-registro para admins", + "allowedFileTypesHelp": "Lista de extensões de arquivo separadas por vírgula", + "featureToggles": "Alternância de Recursos", + "enableAnalytics": "Habilitar rastreamento de análises", + "enableRegistration": "Permitir auto-registro para administradores", "enableShortGalleryUrls": "Usar URLs curtas de galeria", "enableShortGalleryUrlsHelp": "Remove o slug do evento dos novos links de compartilhamento, mantendo os links existentes funcionando.", - "maintenanceMode": "Ativar modo de manutencao", + "maintenanceMode": "Habilitar modo de manutenção", "language": "Idioma", - "defaultLanguage": "Idioma Padrao", + "defaultLanguage": "Idioma Padrão", "defaultLanguageHelp": "Idioma exibido para convidados antes do login", - "defaultWelcomeMessage": "Mensagem de Boas-vindas Padrao", - "welcomeMessage": "Mensagem de Boas-vindas", - "welcomeMessagePlaceholder": "Digite uma mensagem de boas-vindas padrao que sera incluida nos emails de criacao da galeria", - "welcomeMessageHelp": "Esta mensagem sera incluida em todos os emails de criacao de galeria, a menos que seja substituida ao criar um evento", - "saveSettings": "Salvar Configuracoes Gerais", - "saveGeneralSettings": "Salvar Configuracoes Gerais", + "defaultWelcomeMessage": "Mensagem de Boas-Vindas Padrão", + "welcomeMessage": "Mensagem de Boas-Vindas", + "welcomeMessagePlaceholder": "Digite uma mensagem de boas-vindas padrão que será incluída nos emails de criação de galeria", + "welcomeMessageHelp": "Esta mensagem será incluída em todos os emails de criação de galeria, a menos que seja substituída ao criar um evento", + "saveSettings": "Salvar Configurações Gerais", + "saveGeneralSettings": "Salvar Configurações Gerais", "dateTimeFormat": "Formato de Data e Hora", "dateFormat": "Formato de Data", - "dateFormatHelp": "Como as datas sao exibidas em emails e em todo o aplicativo", - "accountSection": "Conta do Admin", - "accountUsername": "Nome de Usuario do Admin", - "accountUsernameHelp": "Exibido na interface de administracao e usado em logs de atividade.", - "accountUsernameRequired": "Nome de usuario e obrigatorio", - "accountUsernameLength": "Nome de usuario deve ter pelo menos 3 caracteres", - "accountEmail": "Email do Admin", - "accountEmailHelp": "Usado para login e recebimento de notificacoes de seguranca.", - "accountEmailRequired": "Endereco de email e obrigatorio", - "accountEmailInvalid": "Digite um endereco de email valido", - "accountSaveButton": "Salvar dados da conta", - "accountSaveSuccess": "Dados da conta atualizados" + "dateFormatHelp": "Como as datas são exibidas nos emails e em todo o aplicativo", + "accountSection": "Conta do Administrador", + "accountUsername": "Nome de Usuário do Administrador", + "accountUsernameHelp": "Exibido na interface administrativa e usado nos registros de atividade.", + "accountUsernameRequired": "Nome de usuário é obrigatório", + "accountUsernameLength": "O nome de usuário deve ter pelo menos 3 caracteres", + "accountEmail": "email do Administrador", + "accountEmailHelp": "Usado para login e recebimento de notificações de segurança.", + "accountEmailRequired": "O endereço de email é obrigatório", + "accountEmailInvalid": "Digite um endereço de email válido", + "accountSaveButton": "Salvar detalhes da conta", + "accountSaveSuccess": "Detalhes da conta atualizados" }, "publicSite": { - "tabLabel": "Site Publico", - "badge": "Pagina Publica", - "title": "Pagina Publica de Destino", - "subtitle": "Publique uma pagina de destino personalizada para convidados quando visitarem seu dominio.", - "loading": "Carregando configuracoes do site publico...", - "enabled": "Ativado", - "disabled": "Desativado", - "htmlLabel": "HTML da Pagina de Destino", - "htmlPlaceholder": "Forneca a marcacao HTML para o hero, secoes e chamadas para acao da sua pagina de destino.", - "htmlHelp": "O HTML bruto e sanitizado ao salvar. Tags inseguras, scripts inline e atributos nao permitidos sao removidos automaticamente. Tokens como {{company_name}}, {{company_tagline}} e {{support_email}} serao substituidos por valores das configuracoes de Marca.", + "tabLabel": "Site Público", + "badge": "Landing Pública", + "title": "Página de Landing Pública", + "subtitle": "Publique uma página de landing personalizada para convidados quando visitarem seu domínio.", + "loading": "Carregando configurações do site público...", + "enabled": "Habilitado", + "disabled": "Desabilitado", + "htmlLabel": "HTML da Página de Landing", + "htmlPlaceholder": "Forneça o código HTML para o hero, seções e chamadas para ação da sua página de landing.", + "htmlHelp": "O HTML bruto é sanitizado ao salvar. Tags inseguras, scripts inline e atributos não permitidos são removidos automaticamente. Tokens como {{company_name}}, {{company_tagline}} e {{support_email}} serão substituídos pelos valores das configurações de Identidade Visual.", "cssLabel": "CSS Personalizado", - "cssPlaceholder": "Substituicoes de CSS opcionais para ajustar layout, cores ou tipografia.", - "cssHelp": "Importacoes e URLs JavaScript sao removidas. Use seletores CSS padrao para substituir o estilo padrao.", - "saveCta": "Salvar site publico", + "cssPlaceholder": "Substituições CSS opcionais para ajustar layout, cores ou tipografia.", + "cssHelp": "Imports e URLs de JavaScript são removidos. Use seletores CSS padrão para substituir o estilo padrão.", + "saveCta": "Salvar site público", "saving": "Salvando...", - "saveSuccess": "Configuracoes do site publico salvas.", - "saveError": "Falha ao salvar configuracoes do site publico.", - "resetCta": "Restaurar padrao", - "resetting": "Restaurando...", - "resetSuccess": "Template do site publico restaurado para os padroes.", - "resetError": "Falha ao restaurar template do site publico.", - "previewTitle": "Visualizacao ao Vivo", - "previewSandboxed": "Visualizacao em sandbox", - "previewDisabled": "Ative o site publico para ver a visualizacao ao vivo.", - "sanitizationNotice": "Scripts, iframes e atributos inseguros sao removidos antes da publicacao. Apenas HTML seguro, links e imagens sao permitidos.", - "htmlRequired": "Forneca conteudo HTML antes de ativar o site publico." + "saveSuccess": "Configurações do site público salvas.", + "saveError": "Falha ao salvar configurações do site público.", + "resetCta": "Redefinir para o padrão", + "resetting": "Redefinindo...", + "resetSuccess": "Modelo do site público redefinido para os padrões.", + "resetError": "Falha ao redefinir modelo do site público.", + "previewTitle": "Pré-visualização ao Vivo", + "previewSandboxed": "Pré-visualização em sandbox", + "previewDisabled": "Habilite o site público para ver a pré-visualização ao vivo.", + "sanitizationNotice": "Scripts, frames incorporados e atributos inseguros são removidos antes da publicação. Apenas HTML seguro, links e imagens são permitidos.", + "htmlRequired": "Forneça conteúdo HTML antes de habilitar o site público." }, "storage": { "title": "Armazenamento", - "overview": "Visao Geral do Armazenamento", - "totalUsed": "Total Usado", + "overview": "Visão Geral do Armazenamento", + "totalUsed": "Total Utilizado", "archiveStorage": "Armazenamento de Arquivos", "storageLimit": "Limite de Armazenamento", - "storageLimitHelper": "Defina um limite para o uso de armazenamento de fotos. Este limite e consultivo e sera destacado quando atingido.", + "storageLimitHelper": "Defina um limite para o uso de armazenamento de fotos. Este limite é indicativo e será destacado quando atingido.", "softLimitInputLabel": "Limite (GB)", - "softLimitHelper": "Digite o uso maximo preferido de armazenamento em gigabytes.", + "softLimitHelper": "Digite o uso máximo de armazenamento preferido em gigabytes.", "recommendedSoftLimit": "Limite Sugerido", "diskCapacity": "Capacidade do Disco", - "diskCapacityReported": "Capacidade do Disco (reportada)", - "diskAvailable": "Disponivel", - "diskAvailableReported": "Disponivel (reportado)", + "diskCapacityReported": "Capacidade do Disco (informada)", + "diskAvailable": "Disponível", + "diskAvailableReported": "Disponível (informado)", "diskFree": "Livre", - "diskFreeReported": "Livre (reportado)", - "diskMetricsUnavailable": "Metricas de disco nao estao disponiveis no Docker Desktop ou ambientes virtualizados.", + "diskFreeReported": "Livre (informado)", + "diskMetricsUnavailable": "As métricas de disco não estão disponíveis no Docker Desktop ou em ambientes virtualizados.", "applyRecommended": "Usar recomendado", - "applyAvailable": "Corresponder ao disponivel", - "invalidSoftLimit": "Digite um numero valido para o limite.", + "applyAvailable": "Corresponder disponível", + "invalidSoftLimit": "Digite um número válido para o limite.", "saveSoftLimit": "Salvar limite", - "limitNotEnforced": "Este limite e apenas consultivo e nao e aplicado automaticamente.", - "overrideTitle": "Substituicao manual de capacidade", - "diskOverrideEnvNote": "A capacidade do disco e controlada por variaveis de ambiente e nao pode ser editada aqui.", - "diskOverrideSettingsHelp": "Defina valores de capacidade personalizados quando o Docker reportar numeros irreais.", + "limitNotEnforced": "Este limite é apenas indicativo e não é aplicado automaticamente.", + "overrideTitle": "Substituição manual de capacidade", + "diskOverrideEnvNote": "A capacidade do disco é controlada por variáveis de ambiente e não pode ser editada aqui.", + "diskOverrideSettingsHelp": "Defina valores de capacidade personalizados quando o Docker reportar números irreais.", "overrideCapacityLabel": "Capacidade total (GB)", - "overrideCapacityHelper": "Deixe em branco para usar a leitura automatica.", - "overrideAvailableLabel": "Espaco disponivel (GB)", - "overrideAvailableHelper": "Opcional. Deixe em branco para calcular a partir da capacidade total menos o armazenamento usado.", - "saveOverride": "Salvar substituicao de capacidade", - "capacityRequiredForAvailable": "Digite uma capacidade total antes de definir o espaco disponivel.", - "availableExceedsCapacity": "O espaco disponivel nao pode exceder a capacidade total.", + "overrideCapacityHelper": "Deixe em branco para usar a leitura automática.", + "overrideAvailableLabel": "Espaço disponível (GB)", + "overrideAvailableHelper": "Opcional. Deixe em branco para calcular a partir da capacidade total menos o armazenamento utilizado.", + "saveOverride": "Salvar substituição de capacidade", + "capacityRequiredForAvailable": "Digite uma capacidade total antes de definir o espaço disponível.", + "availableExceedsCapacity": "O espaço disponível não pode exceder a capacidade total.", "storageUsage": "Uso de Armazenamento", "storageByEvent": "Armazenamento por Evento", "storageManagement": "Gerenciamento de Armazenamento", - "storageManagementHelp": "Considere arquivar ou excluir eventos antigos para liberar espaco de armazenamento. Eventos arquivados sao comprimidos e usam menos armazenamento que galerias ativas.", + "storageManagementHelp": "Considere arquivar ou excluir eventos antigos para liberar espaço de armazenamento. Eventos arquivados são comprimidos e usam menos armazenamento do que galerias ativas.", "noEventsUsingStorage": "Nenhum evento usando armazenamento", "unlimited": "Ilimitado" }, "security": { - "title": "Seguranca", - "passwordSettings": "Configuracoes de Senha", - "minPasswordLength": "Comprimento Minimo da Senha", - "minPasswordLengthHelp": "Numero minimo de caracteres para senhas de galeria", + "title": "Segurança", + "passwordSettings": "Configurações de Senha", + "minPasswordLength": "Comprimento Mínimo da Senha", + "minPasswordLengthHelp": "Número mínimo de caracteres para senhas de galeria", "passwordComplexity": "Complexidade da Senha", - "passwordComplexityHelp": "Nivel de seguranca exigido para senhas de galeria", + "passwordComplexityHelp": "Nível de segurança exigido para senhas de galeria", "complexitySimple": "Simples (6+ caracteres, qualquer texto)", - "complexityModerate": "Moderada (8+ caracteres, maiusculas/minusculas/numeros)", - "complexityStrong": "Forte (12+ caracteres, maiusculas/minusculas/numeros)", + "complexityModerate": "Moderado (8+ caracteres, maiúsculas/números)", + "complexityStrong": "Forte (12+ caracteres, maiúsculas/minúsculas/números)", "complexityVeryStrong": "Muito Forte (12+ caracteres, todos os tipos de caracteres)", - "sessionAuth": "Sessao e Autenticacao", - "sessionTimeout": "Tempo Limite da Sessao (minutos)", - "sessionTimeoutHelp": "Tempo limite da sessao do admin em minutos", - "maxLoginAttempts": "Max Tentativas de Login", - "maxLoginAttemptsHelp": "Maximo de tentativas de login falhadas por IP antes do bloqueio", + "sessionAuth": "Sessão e Autenticação", + "sessionTimeout": "Tempo Limite de Sessão (minutos)", + "sessionTimeoutHelp": "Tempo limite de sessão do administrador em minutos", + "maxLoginAttempts": "Máximo de Tentativas de Login", + "maxLoginAttemptsHelp": "Máximo de tentativas de login com falha por IP antes do bloqueio", "attemptWindowMinutes": "Janela de Tentativas (minutos)", - "attemptWindowMinutesHelp": "Quanto tempo olhar para tras ao contar tentativas de login falhadas", - "lockoutDurationMinutes": "Duracao do Bloqueio (minutos)", - "lockoutDurationMinutesHelp": "Quanto tempo a galeria ou conta permanece bloqueada apos muitas falhas", - "enable2FA": "Ativar autenticacao de dois fatores para admins", - "recaptchaSettings": "Configuracoes do reCAPTCHA", - "enableRecaptcha": "Ativar reCAPTCHA para formularios de login", + "attemptWindowMinutesHelp": "Quanto tempo considerar ao contabilizar tentativas de login com falha", + "lockoutDurationMinutes": "Duração do Bloqueio (minutos)", + "lockoutDurationMinutesHelp": "Por quanto tempo a galeria ou conta fica bloqueada após muitas tentativas falhas", + "enable2FA": "Habilitar autenticação de dois fatores para administradores", + "recaptchaSettings": "Configurações de reCAPTCHA", + "enableRecaptcha": "Habilitar reCAPTCHA para formulários de login", "siteKey": "Chave do Site", - "siteKeyHelp": "Sua chave de site reCAPTCHA v2 (publica)", + "siteKeyHelp": "Sua chave de site reCAPTCHA v2 (pública)", "secretKey": "Chave Secreta", - "secretKeyHelp": "Sua chave secreta reCAPTCHA v2 (manter privada)", + "secretKeyHelp": "Sua chave secreta reCAPTCHA v2 (mantenha privada)", "recaptchaHelp": "Obtenha suas chaves reCAPTCHA em", - "saveSettings": "Salvar Configuracoes de Seguranca", - "saveSecuritySettings": "Salvar Configuracoes de Seguranca" + "saveSettings": "Salvar Configurações de Segurança", + "saveSecuritySettings": "Salvar Configurações de Segurança" }, "categories": { "title": "Categorias", "about": "Sobre Categorias de Fotos", - "aboutText": "Categorias globais estao disponiveis para todos os eventos. Voce tambem pode criar categorias especificas ao editar eventos individuais. Categorias ajudam a organizar fotos e permitem que convidados filtrem fotos por tipo na visualizacao da galeria." + "aboutText": "As categorias globais estão disponíveis para todos os eventos. Você também pode criar categorias específicas do evento ao editar eventos individuais. As categorias ajudam a organizar as fotos e permitem que os convidados filtrem fotos por tipo na visualização da galeria." }, "systemStatus": { "title": "Status do Sistema", - "storageOverview": "Visao Geral do Armazenamento", - "systemInfo": "Informacoes do Sistema", - "databaseInfo": "Informacoes do Banco de Dados", + "storageOverview": "Visão Geral do Armazenamento", + "systemInfo": "Informações do Sistema", + "databaseInfo": "Informações do Banco de Dados", "platform": "Plataforma", - "nodeVersion": "Versao do Node", - "uptime": "Tempo Ativo", - "cpuCores": "Nucleos da CPU", - "memoryUsage": "Uso de Memoria", - "memoryUsed": "Memoria Usada", + "nodeVersion": "Versão do Node", + "uptime": "Tempo de Atividade", + "cpuCores": "Núcleos de CPU", + "memoryUsage": "Uso de Memória", + "memoryUsed": "Memória Utilizada", "photos": "Fotos", - "admins": "Admins", + "admins": "Administradores", "dbSize": "Tamanho do Banco de Dados", - "services": "Servicos em Segundo Plano", + "services": "Serviços em Segundo Plano", "fileWatcher": "Monitoramento de Arquivos", "fileWatcherDesc": "Monitora novas fotos", - "expirationChecker": "Verificador de Expiracao", + "expirationChecker": "Verificador de Expiração", "expirationCheckerDesc": "Arquiva galerias expiradas", "emailProcessor": "Processador de Email", - "emailProcessorDesc": "Envia emails enfileirados", + "emailProcessorDesc": "Envia emails na fila", "emailQueue": "Status da Fila de Email", "pending": "Pendente", "sent": "Enviado", - "failed": "Falhou", - "lastUpdate": "Ultima atualizacao" + "failed": "Com Falha", + "lastUpdate": "Última atualização" }, "photoDimensions": { - "title": "Dimensoes das Fotos", + "title": "Dimensões das Fotos", "totalPhotos": "Total de Fotos", - "withDimensions": "Com Dimensoes", - "missingDimensions": "Dimensoes Ausentes", - "repairButton": "Reparar Dimensoes", + "withDimensions": "Com Dimensões", + "missingDimensions": "Dimensões Ausentes", + "repairButton": "Reparar Dimensões", "repairing": "Reparando...", - "alreadyRunning": "O reparo ja esta em execucao", - "started": "Iniciado reparo de {{count}} fotos", - "noneToRepair": "Todas as fotos ja possuem dimensoes", - "resultSuccess": "Ultimo reparo: {{success}} atualizadas, {{failed}} falharam", - "description": "Preencher largura/altura ausentes para fotos enviadas antes do rastreamento de dimensoes ser adicionado. Necessario para layouts Masonry e Mosaic." + "alreadyRunning": "O reparo já está em andamento", + "started": "Iniciou o reparo de {{count}} fotos", + "noneToRepair": "Todas as fotos já possuem dimensões", + "resultSuccess": "Último reparo: {{success}} atualizadas, {{failed}} com falha", + "description": "Preencha largura/altura ausentes para fotos enviadas antes da implementação do rastreamento de dimensões. Necessário para os layouts Masonry e Mosaico." }, "updateNotifications": { - "title": "Notificacoes de Atualizacao", - "description": "Receba notificacoes por email quando novas versoes do PicPeak estiverem disponiveis.", - "enableEmails": "Ativar notificacoes por email", - "enableEmailsDesc": "Enviar email para admins quando uma nova versao estiver disponivel", - "recipients": "Destinatarios de Email", + "title": "Notificações de Atualização", + "description": "Receba notificações por email quando novas versões do PicPeak estiverem disponíveis.", + "enableEmails": "Habilitar notificações por email", + "enableEmailsDesc": "Enviar email aos administradores quando uma nova versão estiver disponível", + "recipients": "Destinatários do Email", "recipientsPlaceholder": "admin@exemplo.com, outro@exemplo.com", - "recipientsHelper": "Enderecos de email separados por virgula. Deixe vazio para enviar a todos os usuarios admin.", - "lastNotified": "Ultima notificacao enviada para a versao: {{version}}", + "recipientsHelper": "Endereços de email separados por vírgula. Deixe em branco para enviar a todos os usuários administradores.", + "lastNotified": "Última notificação enviada para a versão: {{version}}", "checkNow": "Verificar e Notificar", "sendTest": "Enviar Email de Teste", - "saved": "Configuracoes salvas", - "saveError": "Falha ao salvar configuracoes", - "emailSent": "Email de notificacao enviado para {{count}} destinatarios", - "emailFailed": "Falha ao enviar notificacao", - "checkSuccess": "Notificacao enviada para nova versao", - "checkNoAction": "Nenhuma notificacao necessaria: {{reason}}", - "checkError": "Falha ao verificar atualizacoes" + "saved": "Configurações salvas", + "saveError": "Falha ao salvar configurações", + "emailSent": "Email de notificação enviado para {{count}} destinatários", + "emailFailed": "Falha ao enviar notificação", + "checkSuccess": "Notificação enviada para nova versão", + "checkNoAction": "Nenhuma notificação necessária: {{reason}}", + "checkError": "Falha ao verificar atualizações" }, "events": { - "title": "Criacao de Eventos", - "requiredFields": "Campos Obrigatorios", - "requiredFieldsDescription": "Configure quais campos de contato sao obrigatorios ao criar novos eventos.", + "title": "Criação de Eventos", + "requiredFields": "Campos Obrigatórios", + "requiredFieldsDescription": "Configure quais campos de contato são obrigatórios ao criar novos eventos.", "requireCustomerName": "Exigir nome do cliente", - "requireCustomerNameHelp": "Nome do cliente deve ser fornecido para novos eventos", + "requireCustomerNameHelp": "O nome do cliente deve ser fornecido para novos eventos", "requireCustomerEmail": "Exigir email do cliente", - "requireCustomerEmailHelp": "Email do cliente deve ser fornecido para novos eventos", - "customerEmailWarning": "Necessario para enviar convites da galeria", - "requireAdminEmail": "Exigir email do admin", - "requireAdminEmailHelp": "Email do admin deve ser fornecido para novos eventos", - "adminEmailWarning": "Necessario para receber notificacoes do evento", + "requireCustomerEmailHelp": "O email do cliente deve ser fornecido para novos eventos", + "customerEmailWarning": "Obrigatório para enviar convites de galeria", + "requireAdminEmail": "Exigir email do administrador", + "requireAdminEmailHelp": "O email do administrador deve ser fornecido para novos eventos", + "adminEmailWarning": "Obrigatório para receber notificações de eventos", "requireEventDate": "Exigir data do evento", - "requireEventDateHelp": "Data do evento deve ser fornecida ao criar eventos", - "eventDateWarning": "URLs da galeria usarao identificadores aleatorios em vez de datas", - "requireExpiration": "Exigir data de expiracao", - "requireExpirationHelp": "Galerias devem ter uma data de expiracao", - "expirationWarning": "Galerias sem expiracao permanecerao ativas ate serem arquivadas manualmente", - "saveSettings": "Salvar Configuracoes de Eventos", + "requireEventDateHelp": "A data do evento deve ser fornecida ao criar eventos", + "eventDateWarning": "As URLs da galeria usarão identificadores aleatórios em vez de datas", + "requireExpiration": "Exigir data de expiração", + "requireExpirationHelp": "As galerias devem ter uma data de expiração", + "expirationWarning": "Galerias sem expiração permanecerão ativas até serem arquivadas manualmente", + "saveSettings": "Salvar Configurações de Eventos", "noteTitle": "Nota", - "noteText": "Estas configuracoes afetam apenas a criacao de novos eventos. Eventos existentes nao sao afetados. O comportamento padrao exige todos os campos." + "noteText": "Essas configurações afetam apenas a criação de novos eventos. Eventos existentes não são afetados. O comportamento padrão exige todos os campos." }, "imageSecurity": { - "title": "Protecao de Imagem", - "saveSuccess": "Configuracoes de seguranca de imagem salvas", - "saveError": "Falha ao salvar configuracoes", - "loadError": "Falha ao carregar configuracoes de seguranca de imagem", - "defaultProtection": "Configuracoes de Protecao Padrao", - "defaultProtectionHelp": "Estas configuracoes se aplicam a todos os novos eventos. Eventos individuais podem substituir estes padroes.", - "protectionLevel": "Nivel de Protecao Padrao", - "imageQuality": "Qualidade de Imagem Padrao", - "fragmentationLevel": "Nivel de Fragmentacao", - "enableDevtools": "Ativar deteccao de DevTools por padrao", - "enableCanvas": "Ativar renderizacao canvas por padrao (protecao avancada)", - "rateLimiting": "Limitacao de Taxa", - "rateLimitingHelp": "Limite quantas imagens podem ser solicitadas para prevenir raspagem.", - "requestsPerMinute": "Solicitacoes por minuto", - "requestsPer5Minutes": "Solicitacoes por 5 min", - "requestsPerHour": "Solicitacoes por hora", - "securityMonitoring": "Monitoramento de Seguranca", + "title": "Proteção de Imagens", + "saveSuccess": "Configurações de segurança de imagem salvas", + "saveError": "Falha ao salvar configurações", + "loadError": "Falha ao carregar configurações de segurança de imagem", + "defaultProtection": "Configurações de Proteção Padrão", + "defaultProtectionHelp": "Essas configurações se aplicam a todos os novos eventos. Eventos individuais podem substituir esses padrões.", + "protectionLevel": "Nível de Proteção Padrão", + "imageQuality": "Qualidade de Imagem Padrão", + "fragmentationLevel": "Nível de Fragmentação", + "enableDevtools": "Habilitar detecção de DevTools por padrão", + "enableCanvas": "Habilitar renderização em canvas por padrão (proteção avançada)", + "rateLimiting": "Limitação de Taxa", + "rateLimitingHelp": "Limite quantas imagens podem ser solicitadas para evitar raspagem.", + "requestsPerMinute": "Solicitações por minuto", + "requestsPer5Minutes": "Solicitações por 5 min", + "requestsPerHour": "Solicitações por hora", + "securityMonitoring": "Monitoramento de Segurança", "suspiciousThreshold": "Limite de atividade suspeita", - "autoBlockThreshold": "Limite de bloqueio automatico", - "enableMonitoring": "Ativar monitoramento de seguranca", + "autoBlockThreshold": "Limite de bloqueio automático", + "enableMonitoring": "Habilitar monitoramento de segurança", "blockSuspiciousIps": "Bloquear automaticamente IPs suspeitos", - "logEvents": "Registrar eventos de seguranca no banco de dados", - "infoTitle": "Sobre Protecao de Imagem", - "infoText": "Estes recursos de protecao ajudam a prevenir download e copia casual, mas nao podem bloquear todos os metodos. Usuarios determinados ainda podem encontrar formas de capturar imagens. Considere usar marcas d'agua e acordos legais para protecao abrangente." + "logEvents": "Registrar eventos de segurança no banco de dados", + "infoTitle": "Sobre Proteção de Imagens", + "infoText": "Esses recursos de proteção ajudam a evitar downloads e cópias não autorizados, mas não podem bloquear todos os métodos. Usuários determinados ainda podem encontrar formas de capturar imagens. Considere usar marcas d'água e contratos legais para proteção abrangente." }, "moderation": { - "title": "Moderacao", + "title": "Moderação", "wordFilters": "Filtros de Palavras", - "description": "Gerencie palavras que devem ser filtradas ou bloqueadas em comentarios", + "description": "Gerencie palavras que devem ser filtradas ou bloqueadas nos comentários", "addFilter": "Adicionar Novo Filtro", - "enterWord": "Digite a palavra para filtrar", + "enterWord": "Digite a palavra a filtrar", "searchFilters": "Buscar filtros...", "filterAdded": "Filtro de palavra adicionado com sucesso", - "filterExists": "Este filtro de palavra ja existe", + "filterExists": "Este filtro de palavra já existe", "addError": "Falha ao adicionar filtro de palavra", "filterUpdated": "Filtro de palavra atualizado com sucesso", "updateError": "Falha ao atualizar filtro de palavra", - "filterDeleted": "Filtro de palavra excluido com sucesso", + "filterDeleted": "Filtro de palavra excluído com sucesso", "deleteError": "Falha ao excluir filtro de palavra", "wordRequired": "Por favor, digite uma palavra para filtrar", - "confirmDelete": "Tem certeza que deseja excluir este filtro de palavra?", + "confirmDelete": "Tem certeza de que deseja excluir este filtro de palavra?", "loading": "Carregando filtros de palavras...", "noMatchingFilters": "Nenhum filtro correspondente encontrado", "noFilters": "Nenhum filtro de palavra configurado ainda", @@ -882,19 +882,19 @@ "severityModerate": "Moderado", "severityHigh": "Alto", "severityBlock": "Bloquear", - "severityLevels": "Niveis de Severidade", - "lowDescription": "Palavra e marcada para revisao mas nao e bloqueada automaticamente", - "moderateDescription": "Comentario requer aprovacao manual antes de ficar visivel", - "highDescription": "Comentario e automaticamente ocultado e requer revisao do admin", - "blockDescription": "Comentario e rejeitado imediatamente e nao pode ser enviado" + "severityLevels": "Níveis de Severidade", + "lowDescription": "A palavra é sinalizada para revisão, mas não bloqueada automaticamente", + "moderateDescription": "O comentário requer aprovação manual antes de ser visível", + "highDescription": "O comentário é ocultado automaticamente e requer revisão do administrador", + "blockDescription": "O comentário é rejeitado imediatamente e não pode ser enviado" }, "styling": { "title": "CSS Personalizado" }, "thumbnails": { "title": "Miniaturas", - "dimensionsTitle": "Dimensoes e Qualidade das Miniaturas", - "dimensionsHelp": "Configure o tamanho e qualidade das miniaturas geradas automaticamente. Valores maiores produzem visualizacoes melhores, mas aumentam o armazenamento e o tempo de carregamento.", + "dimensionsTitle": "Dimensões e Qualidade das Miniaturas", + "dimensionsHelp": "Configure o tamanho e a qualidade das miniaturas geradas automaticamente. Valores mais altos produzem pré-visualizações de melhor aparência, mas aumentam o armazenamento e os tempos de carregamento.", "width": "Largura (px)", "widthHelp": "50-1000 pixels", "height": "Altura (px)", @@ -903,124 +903,124 @@ "qualityHelp": "1-100, maior = melhor qualidade mas arquivos maiores", "format": "Formato", "fit": "Modo de Ajuste", - "fitHelp": "Como as imagens sao redimensionadas para caber nas dimensoes da miniatura. \"Cobrir\" corta para preencher, \"Conter\" se ajusta dentro dos limites.", + "fitHelp": "Como as imagens são redimensionadas para caber nas dimensões da miniatura. \"Cobrir\" corta para preencher, \"Conter\" cabe dentro dos limites.", "fit_cover": "Cobrir (cortar para preencher)", - "fit_contain": "Conter (ajustar dentro)", + "fit_contain": "Conter (caber dentro)", "fit_fill": "Preencher (esticar)", - "fit_inside": "Dentro (encolher para caber)", - "fit_outside": "Fora (expandir para cobrir)", + "fit_inside": "Interior (reduzir para caber)", + "fit_outside": "Exterior (expandir para cobrir)", "regenerateTitle": "Regenerar Miniaturas", - "regenerateHelp": "Apos alterar as configuracoes de miniaturas, regenere todas as miniaturas existentes para aplicar a nova configuracao. Isto roda em segundo plano e pode demorar para galerias grandes.", + "regenerateHelp": "Após alterar as configurações de miniaturas, regenere todas as miniaturas existentes para aplicar a nova configuração. Isso é executado em segundo plano e pode demorar para galerias grandes.", "regenerateButton": "Regenerar Todas as Miniaturas", - "regenerateStarted": "Regeneracao de miniaturas iniciada", - "regenerateError": "Falha ao iniciar regeneracao de miniaturas", - "saveSuccess": "Configuracoes de miniaturas salvas", - "saveError": "Falha ao salvar configuracoes de miniaturas", - "loadError": "Falha ao carregar configuracoes de miniaturas", + "regenerateStarted": "Regeneração de miniaturas iniciada", + "regenerateError": "Falha ao iniciar regeneração de miniaturas", + "saveSuccess": "Configurações de miniaturas salvas", + "saveError": "Falha ao salvar configurações de miniaturas", + "loadError": "Falha ao carregar configurações de miniaturas", "infoTitle": "Sobre Miniaturas", - "infoText": "Miniaturas sao imagens de visualizacao menores geradas a partir dos originais. Aumentar o tamanho ou qualidade melhora a aparencia das fotos na grade da galeria, mas usa mais armazenamento e largura de banda. Apos alterar as configuracoes, use \"Regenerar Todas as Miniaturas\" para atualizar fotos existentes." + "infoText": "As miniaturas são imagens de pré-visualização menores geradas a partir dos originais. Aumentar o tamanho ou a qualidade melhora a aparência das fotos na grade da galeria, mas usa mais armazenamento e largura de banda. Após alterar as configurações, use \"Regenerar Todas as Miniaturas\" para atualizar as fotos existentes." }, "seo": { - "title": "SEO e Robots", - "indexingTitle": "Indexacao por Mecanismos de Busca", - "allowIndexing": "Permitir indexacao por mecanismos de busca", - "allowIndexingHelp": "Quando desativado, todos os rastreadores sao bloqueados via robots.txt. Recomendado desativar para plataformas privadas de fotos.", + "title": "SEO e Robôs", + "indexingTitle": "Indexação por Mecanismos de Busca", + "allowIndexing": "Permitir indexação por mecanismos de busca", + "allowIndexingHelp": "Quando desabilitado, todos os rastreadores são bloqueados via robots.txt. Recomendado desativado para plataformas de fotos privadas.", "sitemapUrl": "URL do Sitemap", "sitemapUrlHelp": "Opcional. Adicionado ao robots.txt se fornecido.", "aiBlockingTitle": "Bloqueio de IA e Bots", "blockAiCrawlers": "Bloquear rastreadores de IA/LLM", - "blockAiCrawlersHelp": "Impedir que bots de treinamento de IA acessem seu conteudo.", + "blockAiCrawlersHelp": "Impedir que bots de treinamento de IA acessem seu conteúdo.", "blockedAgents": "Agentes de IA bloqueados", "addAgentPlaceholder": "Digite o nome do agente...", - "blockSocialBots": "Bloquear bots de pre-visualizacao de redes sociais", - "blockSocialBotsHelp": "Impedir pre-visualizacoes de links no Twitter, Facebook, LinkedIn, etc.", + "blockSocialBots": "Bloquear bots de pré-visualização de redes sociais", + "blockSocialBotsHelp": "Impedir pré-visualizações de links no Twitter, Facebook, LinkedIn, etc.", "metaTagsTitle": "Meta Tags e Regras Personalizadas", "metaNoindex": "Adicionar meta tag noindex", - "metaNoindexHelp": "Informa aos mecanismos de busca para nao indexar paginas (nivel HTML, complementa robots.txt).", + "metaNoindexHelp": "Informa aos mecanismos de busca para não indexar páginas (nível HTML, complementa robots.txt).", "metaNofollow": "Adicionar meta tag nofollow", - "metaNofollowHelp": "Informa aos mecanismos de busca para nao seguir links nas paginas.", + "metaNofollowHelp": "Informa aos mecanismos de busca para não seguir links nas páginas.", "metaNoai": "Adicionar meta tag noai/noimageai", - "metaNoaiHelp": "Sinaliza que o conteudo nao deve ser usado para treinamento de IA.", - "customRules": "Regras personalizadas do robots.txt", + "metaNoaiHelp": "Sinaliza que o conteúdo não deve ser usado para treinamento de IA.", + "customRules": "Regras personalizadas de robots.txt", "ruleAgentPlaceholder": "User-agent", - "rulePathPlaceholder": "Caminho de bloqueio", - "showPreview": "Mostrar visualizacao do robots.txt", - "hidePreview": "Ocultar visualizacao do robots.txt", - "saveSettings": "Salvar Configuracoes de SEO" + "rulePathPlaceholder": "Caminho para Disallow", + "showPreview": "Mostrar pré-visualização do robots.txt", + "hidePreview": "Ocultar pré-visualização do robots.txt", + "saveSettings": "Salvar Configurações de SEO" }, "analytics": { - "title": "Analiticos", - "umamiIntegration": "Integracao com Umami Analytics", - "enableUmami": "Ativar Umami Analytics", + "title": "Análises", + "umamiIntegration": "Integração com Umami Analytics", + "enableUmami": "Habilitar Umami Analytics", "umamiUrl": "URL do Umami", - "umamiUrlHelp": "A URL da sua instancia Umami (ex., https://analytics.seudominio.com)", + "umamiUrlHelp": "A URL da sua instância do Umami (ex.: https://analytics.seudominio.com)", "websiteId": "ID do Site", - "websiteIdHelp": "Seu ID de site Umami (encontrado no painel do Umami)", + "websiteIdHelp": "O ID do seu site no Umami (encontrado no painel do Umami)", "shareUrl": "URL de Compartilhamento (Opcional)", - "shareUrlHelp": "URL publica de compartilhamento para incorporar o painel completo (criar no Umami)", - "umamiInfo": "Sobre Umami Analytics", - "umamiInfoText": "Umami e uma plataforma de analiticos de codigo aberto focada em privacidade. Rastreia visualizacoes de pagina, visitantes unicos e eventos personalizados sem usar cookies.", - "learnMore": "Saiba mais sobre Umami", - "saveAnalyticsSettings": "Salvar Configuracoes de Analiticos", - "backendAnalytics": "Analiticos do Backend", - "backendAnalyticsText": "O sistema tambem rastreia analiticos basicos no servidor para monitoramento de seguranca e desempenho.", - "tracked": "Metricas Rastreadas", - "galleryViews": "Visualizacoes de pagina da galeria", + "shareUrlHelp": "URL pública de compartilhamento para incorporar o painel completo (criar no Umami)", + "umamiInfo": "Sobre o Umami Analytics", + "umamiInfoText": "O Umami é uma plataforma de análises open-source focada em privacidade. Rastreia visualizações de página, visitantes únicos e eventos personalizados sem usar cookies.", + "learnMore": "Saiba mais sobre o Umami", + "saveAnalyticsSettings": "Salvar Configurações de Análises", + "backendAnalytics": "Análises do Backend", + "backendAnalyticsText": "O sistema também rastreia análises básicas no lado do servidor para monitoramento de segurança e desempenho.", + "tracked": "Métricas Rastreadas", + "galleryViews": "Visualizações de página da galeria", "photoDownloads": "Downloads individuais e em massa", - "uniqueVisitors": "Visitantes unicos por IP", - "deviceTypes": "Tipos de dispositivo a partir de user agents", + "uniqueVisitors": "Visitantes únicos por IP", + "deviceTypes": "Tipos de dispositivo por user agents", "privacy": "Privacidade", - "privacyText": "Enderecos IP sao hasheados para privacidade. Nenhum dado pessoal e armazenado. Dados de analiticos sao retidos por 90 dias." + "privacyText": "Os endereços IP são hasheados para privacidade. Nenhum dado pessoal é armazenado. Os dados de análises são retidos por 90 dias." } }, "analytics": { - "title": "Painel de Analiticos", - "titleSimple": "Analiticos", - "subtitle": "Acompanhe o desempenho da galeria e o engajamento dos visitantes", - "detailedSubtitle": "Analiticos detalhados fornecidos pelo Umami", - "loadingAnalytics": "Carregando analiticos...", - "showSummaryView": "Mostrar Visao Resumida", + "title": "Painel de Análises", + "titleSimple": "Análises", + "subtitle": "Acompanhe o desempenho das galerias e o engajamento dos visitantes", + "detailedSubtitle": "Análises detalhadas fornecidas pelo Umami", + "loadingAnalytics": "Carregando análises...", + "showSummaryView": "Mostrar Visão Resumida", "fullDashboard": "Painel Completo", "refresh": "Atualizar", - "last7Days": "Ultimos 7 dias", - "last30Days": "Ultimos 30 dias", - "last90Days": "Ultimos 90 dias", - "pageViews": "Visualizacoes de Pagina", - "uniqueVisitors": "Visitantes Unicos", + "last7Days": "Últimos 7 dias", + "last30Days": "Últimos 30 dias", + "last90Days": "Últimos 90 dias", + "pageViews": "Visualizações de Página", + "uniqueVisitors": "Visitantes Únicos", "totalDownloads": "Total de Downloads", - "topGallery": "Galeria Principal", - "topPages": "Paginas Principais", - "views": "visualizacoes", - "visitors": "visitantes unicos", + "topGallery": "Galeria Mais Visitada", + "topPages": "Páginas Mais Visitadas", + "views": "visualizações", + "visitors": "visitantes únicos", "topDownloadsByGallery": "Top Downloads por Galeria", - "deviceBreakdown": "Distribuicao de Dispositivos", + "deviceBreakdown": "Distribuição por Dispositivo", "desktop": "Desktop", - "mobile": "Celular", + "mobile": "Mobile", "tablet": "Tablet", "storageUsage": "Uso de Armazenamento", - "used": "Usado", + "used": "Utilizado", "of": "de", "totalPhotos": "Total de Fotos", "activeEvents": "Eventos Ativos", - "notConfigured": "Umami Analytics Nao Configurado", - "configureInstructions": "Para ver dados reais de analiticos, configure o Umami nas variaveis de ambiente e configuracoes do painel admin.", - "noData": "Nenhum dado disponivel", - "percentChange": "{{percent}}% em relacao ao periodo anterior" + "notConfigured": "Umami Analytics Não Configurado", + "configureInstructions": "Para ver dados reais de análises, configure o Umami nas variáveis de ambiente e nas configurações do painel administrativo.", + "noData": "Nenhum dado disponível", + "percentChange": "{{percent}}% em relação ao período anterior" }, "branding": { - "title": "Marca e Temas", - "titleFull": "Marca e Personalizacao", - "subtitle": "Personalize a aparencia das suas galerias", - "loadingBranding": "Carregando configuracoes de marca...", + "title": "Identidade Visual e Temas", + "titleFull": "Identidade Visual e Personalização", + "subtitle": "Personalize a aparência das suas galerias", + "loadingBranding": "Carregando configurações de identidade visual...", "themeAndStyle": "Tema e Estilo", - "companyInfo": "Informacoes da Empresa", + "companyInfo": "Informações da Empresa", "companyName": "Nome da Empresa", - "companyNameHelp": "Exibido nos cabecalhos da galeria e emails", + "companyNameHelp": "Exibido nos cabeçalhos das galerias e nos emails", "companyTagline": "Slogan da Empresa", - "companyTaglineHelp": "Uma breve descricao do seu negocio", + "companyTaglineHelp": "Uma breve descrição do seu negócio", "supportEmail": "Email de Suporte", - "supportEmailHelp": "Email de contato para suporte aos convidados", - "footerText": "Texto do Rodape", + "supportEmailHelp": "email de contato para suporte aos convidados", + "footerText": "Texto do Rodapé", "footerTextHelp": "Exibido na parte inferior das galerias", "logo": "Logo", "currentLogo": "Logo atual", @@ -1032,137 +1032,137 @@ "uploadFavicon": "Enviar Favicon", "removeFavicon": "Remover Favicon", "faviconHelp": "Formato PNG ou ICO, tamanho recomendado: 32x32px", - "watermark": "Marca d'agua", - "watermarkSettings": "Configuracoes de Marca d'agua", - "enableWatermarks": "Ativar Marcas d'agua", - "watermarkHelp": "Adicione o nome da sua empresa como marca d'agua nas fotos baixadas", - "watermarkLogo": "Logo da Marca d'agua", - "currentWatermark": "Marca d'agua atual", - "uploadWatermarkLogo": "Enviar Logo da Marca d'agua", - "watermarkPosition": "Posicao da Marca d'agua", + "watermark": "Marca d'Água", + "watermarkSettings": "Configurações de Marca d'Água", + "enableWatermarks": "Habilitar Marcas d'Água", + "watermarkHelp": "Adicione o nome da sua empresa como marca d'água nas fotos baixadas", + "watermarkLogo": "Logo da Marca d'Água", + "currentWatermark": "Marca d'água atual", + "uploadWatermarkLogo": "Enviar Logo para Marca d'Água", + "watermarkPosition": "Posição da Marca d'Água", "topLeft": "Superior Esquerdo", "topRight": "Superior Direito", "center": "Centro", "bottomLeft": "Inferior Esquerdo", "bottomRight": "Inferior Direito", - "watermarkOpacity": "Opacidade da Marca d'agua", - "watermarkSize": "Tamanho da Marca d'agua", + "watermarkOpacity": "Opacidade da Marca d'Água", + "watermarkSize": "Tamanho da Marca d'Água", "theme": "Tema", "galleryTheme": "Tema da Galeria", - "themeCustomization": "Personalizacao do Tema", + "themeCustomization": "Personalização do Tema", "selectPreset": "Selecione um tema predefinido", "colors": "Cores", - "primaryColor": "Cor Primaria", - "secondaryColor": "Cor Secundaria", + "primaryColor": "Cor Primária", + "secondaryColor": "Cor Secundária", "accentColor": "Cor de Destaque", "backgroundColor": "Cor de Fundo", "textColor": "Cor do Texto", "colorMode": "Modo de Cor", "colorModeLight": "Claro", "colorModeDark": "Escuro", - "colorModeAuto": "Automatico", - "colorModeHelp": "Automatico segue a preferencia do sistema do visitante.", + "colorModeAuto": "Automático", + "colorModeHelp": "Automático segue a preferência do sistema do visitante.", "customCSS": "CSS Personalizado", - "preview": "Visualizar", - "previewInNewTab": "Visualizar em Nova Aba", + "preview": "Pré-visualização", + "previewInNewTab": "Pré-visualizar em Nova Aba", "reset": "Redefinir", - "saveChanges": "Salvar Alteracoes", - "applyLivePreview": "Aplicar alteracoes imediatamente (Visualizacao ao Vivo)", - "eventSpecificThemes": "Temas Especificos do Evento", - "eventThemesInfo": "Voce pode substituir estas configuracoes globais de tema para eventos individuais ao cria-los ou edita-los.", - "themePresets": "Predefinicoes de Tema", + "saveChanges": "Salvar Alterações", + "applyLivePreview": "Aplicar alterações imediatamente (Pré-visualização ao Vivo)", + "eventSpecificThemes": "Temas Específicos do Evento", + "eventThemesInfo": "Você pode substituir essas configurações de tema globais para eventos individuais ao criá-los ou editá-los.", + "themePresets": "Temas Predefinidos", "galleryLayout": "Layout da Galeria", "layoutDescriptions": { - "grid": "Layout de grade classico com tamanhos de foto consistentes", + "grid": "Layout de grade clássico com tamanhos de foto uniformes", "masonry": "Layout estilo Pinterest com alturas variadas", - "carousel": "Apresentacao em tela cheia com navegacao", + "carousel": "Slideshow em tela cheia com navegação", "timeline": "Fotos organizadas por data", "hero": "Imagem em destaque com grade abaixo", - "mosaic": "Layout artistico com tamanhos variados", - "justified": "Layout baseado em linhas preservando proporcoes", + "mosaic": "Layout artístico com tamanhos mistos", + "justified": "Layout baseado em linhas preservando as proporções", "gallery-premium": "Tema claro elegante com hero e masonry (Beta)", - "gallery-story": "Tema escuro cinematico com secoes de cena (Beta)" + "gallery-story": "Tema escuro cinematográfico com seções de cenas (Beta)" }, - "layoutSettings": "Configuracoes de Layout", - "photoSpacing": "Espacamento de Fotos", + "layoutSettings": "Configurações de Layout", + "photoSpacing": "Espaçamento das Fotos", "spacing": { - "tight": "Apertado", + "tight": "Compacto", "normal": "Normal", - "relaxed": "Espaco" + "relaxed": "Espaçado" }, - "photoAnimation": "Animacao de Fotos", + "photoAnimation": "Animação das Fotos", "animation": { "none": "Nenhuma", - "fade": "Desvanecer", + "fade": "Fade", "scale": "Escala", - "slide": "Deslizar" + "slide": "Deslizamento" }, "columns": "Colunas", - "mobile": "Celular", + "mobile": "Mobile", "tablet": "Tablet", "desktop": "Desktop", - "enableAutoplay": "Ativar Reproducao Automatica", - "autoplayInterval": "Intervalo de Reproducao (segundos)", + "enableAutoplay": "Habilitar Reprodução Automática", + "autoplayInterval": "Intervalo de Reprodução Automática (segundos)", "groupPhotosBy": "Agrupar Fotos Por", "grouping": { "day": "Dia", "week": "Semana", - "month": "Mes" + "month": "Mês" }, "masonryMode": "Modo de Layout", "masonryModeOptions": { "columns": "Colunas (estilo Pinterest)", - "rows": "Linhas (justificado personalizado)", - "flickr": "Flickr (justificado consagrado)", - "justified": "Google Photos (algoritmo Knuth-Plass)" + "rows": "Linhas (Justified personalizado)", + "flickr": "Flickr (Justified testado)", + "justified": "Google Fotos (algoritmo Knuth-Plass)" }, "masonryModeHint": { "columns": "Colunas verticais estilo Pinterest com alturas variadas", - "rows": "Layout justificado personalizado baseado em linhas", - "flickr": "Algoritmo de layout justificado de codigo aberto do Flickr", - "justified": "Linhas estilo Google Photos usando algoritmo Knuth-Plass para quebras otimas" + "rows": "Layout justified por linhas personalizado", + "flickr": "Algoritmo de layout justified open-source do Flickr", + "justified": "Layout de linhas estilo Google Fotos usando o algoritmo Knuth-Plass para quebras otimizadas" }, "targetRowHeight": "Altura Alvo da Linha", - "targetRowHeightHint": "Altura em pixels (150-400). Fotos serao dimensionadas para caber nas linhas.", - "lastRowBehavior": "Alinhamento da Ultima Linha", + "targetRowHeightHint": "Altura em pixels (150-400). As fotos serão dimensionadas para caber nas linhas.", + "lastRowBehavior": "Alinhamento da Última Linha", "lastRowOptions": { - "left": "Alinhado a esquerda", + "left": "Alinhado à esquerda", "center": "Centralizado", - "justify": "Justificado (esticar)" + "justify": "Justified (esticado)" }, - "showHeroSection": "Mostrar Secao Hero", - "showHeroSectionHint": "Exibir uma imagem hero em destaque acima da galeria justificada", - "heroHeight": "Altura da Secao Hero", + "showHeroSection": "Mostrar Seção Hero", + "showHeroSectionHint": "Exibir uma imagem hero em destaque acima da galeria justified", + "heroHeight": "Altura da Seção Hero", "heroHeightOptions": { "small": "Pequena (40-50%)", - "medium": "Media (50-70%)", + "medium": "Média (50-70%)", "large": "Grande (60-80%)" }, - "heroOverlayOpacity": "Opacidade da Sobreposicao Hero", - "heroOverlayHint": "Escurecer a imagem hero para melhorar a legibilidade do texto", + "heroOverlayOpacity": "Opacidade do Overlay Hero", + "heroOverlayHint": "Escureça a imagem hero para melhorar a legibilidade do texto", "typographyAndStyle": "Tipografia e Estilo", "bodyFont": "Fonte do Corpo", - "headingFont": "Fonte do Titulo", - "sameAsBody": "Mesma do corpo", + "headingFont": "Fonte dos Títulos", + "sameAsBody": "Igual ao corpo", "fontSize": "Tamanho da Fonte", "fontSizes": { "small": "Pequeno", "normal": "Normal", "large": "Grande" }, - "borderRadius": "Raio da Borda", + "borderRadius": "Arredondamento das Bordas", "borderRadiusOptions": { "none": "Nenhum", "small": "Pequeno", - "medium": "Medio", + "medium": "Médio", "large": "Grande" }, "shadowStyle": "Estilo de Sombra", "shadowOptions": { - "none": "Nenhum", + "none": "Nenhuma", "subtle": "Sutil", "normal": "Normal", - "dramatic": "Dramatico" + "dramatic": "Dramática" }, "backgroundPattern": "Fundo", "backgroundOptions": { @@ -1171,84 +1171,84 @@ "grid": "Grade", "waves": "Ondas" }, - "customCSSHelp": "Avancado: Adicione CSS personalizado para customizar ainda mais a aparencia", + "customCSSHelp": "Avançado: Adicione CSS personalizado para personalizar ainda mais a aparência", "cssInstructions": { "title": "Como usar CSS Personalizado", - "variables": "Variaveis CSS do Tema", - "variablesDesc": "Use estas variaveis CSS para combinar com suas predefinicoes de tema:", - "layouts": "Layouts Personalizados de Galeria", - "layoutsDesc": "Direcione elementos da galeria com estes seletores:", + "variables": "Variáveis CSS do Tema", + "variablesDesc": "Use essas variáveis CSS para corresponder aos seus temas predefinidos:", + "layouts": "Layouts de Galeria Personalizados", + "layoutsDesc": "Direcione elementos da galeria com esses seletores:", "glassEffect": "Efeito Glassmorphism", - "glassEffectDesc": "Crie efeitos modernos de vidro:", + "glassEffectDesc": "Crie efeitos de vidro modernos:", "tip": "Dica", - "tipText": "Use Templates CSS em Configuracoes > Templates CSS para designs pre-construidos como Apple Liquid Glass." + "tipText": "Use Modelos de CSS em Configurações > Modelos CSS para designs pré-construídos como Apple Liquid Glass." }, - "resetToDefault": "Restaurar Padrao", + "resetToDefault": "Redefinir para Padrão", "applyTheme": "Aplicar Tema", "customTheme": "Tema Personalizado", "customizeTheme": "Personalizar Tema", "saveTheme": "Salvar Tema", - "previewLayout": "Visualizar Layout", - "livePreview": "Visualizacao ao Vivo", - "heroPlaceholderText": "A foto hero do seu evento aparecera aqui", + "previewLayout": "Pré-visualizar Layout", + "livePreview": "Pré-visualização ao Vivo", + "heroPlaceholderText": "A foto hero do seu evento aparecerá aqui", "whiteLabel": "White Label", - "hidePoweredBy": "Ocultar marca \"Powered by PicPeak\"", - "hidePoweredByHelp": "Remover a atribuicao PicPeak dos rodapes da galeria para uma experiencia totalmente white-label", - "logoCustomization": "Personalizacao do Logo", + "hidePoweredBy": "Ocultar marca \"Desenvolvido por PicPeak\"", + "hidePoweredByHelp": "Remover a atribuição do PicPeak dos rodapés das galerias para uma experiência totalmente white-label", + "logoCustomization": "Personalização do Logo", "changeLogo": "Alterar Logo", "logoSizeSmall": "Pequeno (32px)", - "logoSizeMedium": "Medio (48px)", + "logoSizeMedium": "Médio (48px)", "logoSizeLarge": "Grande (64px)", "logoSizeXLarge": "Extra Grande (96px)", "logoSizeCustom": "Personalizado", - "logoMaxHeight": "Altura Maxima (pixels)", - "logoMaxHeightHelp": "Defina uma altura maxima personalizada para o logo (20-200 pixels)", - "logoPosition": "Posicao do Logo no Cabecalho", + "logoMaxHeight": "Altura Máxima (pixels)", + "logoMaxHeightHelp": "Defina uma altura máxima personalizada para o logo (20-200 pixels)", + "logoPosition": "Posição do Logo no Cabeçalho", "positionLeft": "Esquerda", "positionCenter": "Centro", "positionRight": "Direita", - "logoDisplayMode": "Modo de Exibicao", + "logoDisplayMode": "Modo de Exibição", "logoOnly": "Apenas Logo", "textOnly": "Apenas Nome da Empresa", "logoAndText": "Logo e Nome da Empresa", - "showLogoInHeader": "Mostrar logo no cabecalho da galeria", - "showLogoInHeaderHelp": "Exibir o logo na barra principal do cabecalho", - "showLogoInHero": "Mostrar logo na secao hero", - "showLogoInHeroHelp": "Exibir o logo nas secoes hero (para layouts nao-grade)", - "headerStyle": "Estilo do Cabecalho", - "headerStyleDescription": "Escolha como o cabecalho da galeria aparece. O estilo do cabecalho e independente do layout das fotos.", + "showLogoInHeader": "Mostrar logo no cabeçalho da galeria", + "showLogoInHeaderHelp": "Exibir o logo na barra de cabeçalho principal", + "showLogoInHero": "Mostrar logo na seção hero", + "showLogoInHeroHelp": "Exibir o logo nas seções hero (para layouts não-grade)", + "headerStyle": "Estilo do Cabeçalho", + "headerStyleDescription": "Escolha como o cabeçalho da galeria é exibido. O estilo do cabeçalho é independente do layout das fotos.", "headerStyleOptions": { "hero": "Imagem Hero", - "standard": "Banner Padrao", + "standard": "Banner Padrão", "minimal": "Minimalista", - "none": "Sem Cabecalho" + "none": "Sem Cabeçalho" }, "headerStyleDescriptions": { - "hero": "Imagem em altura total com sobreposicao de informacoes do evento", - "standard": "Banner classico com detalhes do evento", - "minimal": "Cabecalho compacto com informacoes essenciais", - "none": "Ocultar cabecalho completamente" + "hero": "Imagem em altura total com sobreposição de informações do evento", + "standard": "Banner clássico com detalhes do evento", + "minimal": "Cabeçalho compacto com informações essenciais", + "none": "Ocultar cabeçalho completamente" }, "heroDividerStyle": "Estilo do Divisor", - "heroDividerDescription": "Escolha como a transicao entre a imagem hero e o conteudo da galeria aparece.", + "heroDividerDescription": "Escolha como é a transição entre a imagem hero e o conteúdo da galeria.", "dividerOptions": { "wave": "Onda", "straight": "Reto", - "angle": "Angulo", + "angle": "Ângulo", "curve": "Curva", "none": "Nenhum" }, "controlsStyle": "Estilo dos Controles", - "controlsStyleDescription": "Escolha como os filtros e controles da galeria sao exibidos.", + "controlsStyleDescription": "Escolha como os filtros e controles da galeria são exibidos.", "controlsStyleOptions": { - "classic": "Classico", + "classic": "Clássico", "sidebar": "Barra Lateral" }, "controlsStyleDescriptions": { - "classic": "Barra de filtros inline abaixo do cabecalho", - "sidebar": "Botao de menu abre barra lateral com filtros" + "classic": "Barra de filtros inline abaixo do cabeçalho", + "sidebar": "Botão de menu abre barra lateral com filtros" }, - "controlsStyleHeroWarning": "Barra lateral e recomendada para cabecalhos hero para evitar que os controles aparecam acima da imagem hero." + "controlsStyleHeroWarning": "A barra lateral é recomendada para cabeçalhos hero para evitar que os controles apareçam acima da imagem hero." }, "admin": { "title": "Painel Admin", @@ -1262,172 +1262,172 @@ "storagePercent": "{{percent}}% do limite {{limit}}", "storageSoftLimitConfigured": "Limite configurado: {{limit}}", "storageSoftLimitRecommended": "Limite sugerido: {{limit}}", - "version": "Versao", + "version": "Versão", "archivedEvents": "Eventos Arquivados", - "systemHealth": "Saude do Sistema", + "systemHealth": "Saúde do Sistema", "health": { - "healthy": "Saudavel", - "warning": "Alerta", + "healthy": "Saudável", + "warning": "Aviso", "error": "Erro", "checking": "Verificando..." }, "updates": { - "available": "Atualizacao Disponivel", - "newVersion": "Versao {{version}} esta disponivel", + "available": "Atualização Disponível", + "newVersion": "A versão {{version}} está disponível", "currentVersion": "Atual: {{version}}", "channel": "Canal: {{channel}}", - "channelStable": "Estavel", + "channelStable": "Estável", "channelBeta": "Beta", "beta": "BETA", - "viewReleaseNotes": "Ver Notas de Lancamento", - "updateAvailableShort": "v{{version}} disponivel", - "checkForUpdates": "Verificar Atualizacoes", - "upToDate": "Voce esta atualizado", - "lastChecked": "Ultima verificacao: {{time}}", + "viewReleaseNotes": "Ver Notas de Versão", + "updateAvailableShort": "v{{version}} disponível", + "checkForUpdates": "Verificar Atualizações", + "upToDate": "Você está atualizado", + "lastChecked": "Última verificação: {{time}}", "updateNow": "Atualizar Agora", "updateDialog": { "title": "Atualizar PicPeak", "detectedEnv": "Ambiente Detectado", "beforeUpdating": "Antes de atualizar:", - "updateCommands": "Comandos de Atualizacao:", - "afterUpdating": "Apos atualizar:", + "updateCommands": "Comandos de Atualização:", + "afterUpdating": "Após atualizar:", "copyAllCommands": "Copiar Todos os Comandos", - "completeChecklist": "Complete a lista de verificacao antes de atualizar", - "error": "Falha ao carregar instrucoes de atualizacao" + "completeChecklist": "Complete a lista de verificação antes de atualizar", + "error": "Falha ao carregar as instruções de atualização" } }, - "notifications": "Notificacoes", - "viewAllNotifications": "Ver todas as notificacoes", - "noNotifications": "Nenhuma nova notificacao", + "notifications": "Notificações", + "viewAllNotifications": "Ver todas as notificações", + "noNotifications": "Nenhuma notificação nova", "markAllRead": "Marcar todas como lidas", - "clearAll": "Limpar todas", + "clearAll": "Limpar tudo", "close": "Fechar", - "noNotificationsMessage": "Sem notificacoes", + "noNotificationsMessage": "Nenhuma notificação", "notificationMessages": { "eventCreated": "Novo evento \"{{eventName}}\" foi criado", "eventArchived": "Evento \"{{eventName}}\" foi arquivado", "eventUpdated": "Evento \"{{eventName}}\" foi atualizado", - "eventDeleted": "Evento \"{{eventName}}\" foi excluido", + "eventDeleted": "Evento \"{{eventName}}\" foi excluído", "photosUploaded": "{{count}} fotos enviadas para \"{{eventName}}\"", - "photoDeleted": "Foto excluida de \"{{eventName}}\"", - "photosBulkDeleted": "{{count}} fotos excluidas de \"{{eventName}}\"", - "eventExpiring": "Evento \"{{eventName}}\" expira em {{days}} dias", - "eventExpired": "Evento \"{{eventName}}\" expirou", + "photoDeleted": "Foto excluída de \"{{eventName}}\"", + "photosBulkDeleted": "{{count}} fotos excluídas de \"{{eventName}}\"", + "eventExpiring": "O evento \"{{eventName}}\" expira em {{days}} dias", + "eventExpired": "O evento \"{{eventName}}\" expirou", "passwordChanged": "Senha alterada por {{actorName}}", "passwordReset": "Senha redefinida para \"{{eventName}}\"", - "settingsUpdated": "Configuracoes de {{type}} atualizadas", - "emailTemplateUpdated": "Template de email \"{{template}}\" atualizado", + "settingsUpdated": "Configurações de {{type}} atualizadas", + "emailTemplateUpdated": "Modelo de email \"{{template}}\" atualizado", "bulkDownload": "{{count}} fotos baixadas de \"{{eventName}}\"", "storageWarning": "Uso de armazenamento em {{percentage}}%", - "adminLogout": "Admin {{actorName}} fez logout", + "adminLogout": "Admin {{actorName}} encerrou a sessão", "categoryCreated": "Categoria \"{{name}}\" criada para \"{{eventName}}\"", "categoryUpdated": "Categoria \"{{name}}\" atualizada para \"{{eventName}}\"", - "categoryDeleted": "Categoria \"{{name}}\" excluida de \"{{eventName}}\"", - "cmsPageUpdated": "Pagina CMS \"{{slug}}\" atualizada", - "emailConfigUpdated": "Configuracao de email atualizada", + "categoryDeleted": "Categoria \"{{name}}\" excluída de \"{{eventName}}\"", + "cmsPageUpdated": "Página CMS \"{{slug}}\" atualizada", + "emailConfigUpdated": "Configuração de email atualizada", "faviconUploaded": "Favicon enviado", - "brandingUpdated": "Configuracoes de marca atualizadas", - "generalSettingsUpdated": "Configuracoes gerais atualizadas", - "securitySettingsUpdated": "Configuracoes de seguranca atualizadas", - "themeUpdated": "Configuracoes de tema atualizadas", + "brandingUpdated": "Configurações de identidade visual atualizadas", + "generalSettingsUpdated": "Configurações gerais atualizadas", + "securitySettingsUpdated": "Configurações de segurança atualizadas", + "themeUpdated": "Configurações de tema atualizadas", "archiveDownloaded": "Arquivo baixado para \"{{eventName}}\"", - "archiveDeleted": "Arquivo excluido para \"{{eventName}}\"", + "archiveDeleted": "Arquivo excluído para \"{{eventName}}\"", "archiveRestored": "Arquivo restaurado para \"{{eventName}}\"", "systemActivity": "Atividade do sistema: {{type}}", "adminProfileUpdated": "Perfil admin atualizado por {{actorName}}" }, "notificationToasts": { - "markedAllRead": "Todas as notificacoes marcadas como lidas", - "clearedAll": "{{count}} notificacoes limpas", + "markedAllRead": "Todas as notificações marcadas como lidas", + "clearedAll": "{{count}} notificações apagadas", "profileUpdated": "Perfil admin atualizado" }, "markAsRead": "Marcar como lida", "markAllAsRead": "Marcar todas como lidas", - "notificationSettings": "Configuracoes de Notificacao", + "notificationSettings": "Configurações de Notificação", "changePassword": "Alterar Senha", "darkMode": "Mudar para modo escuro", "lightMode": "Mudar para modo claro", "accountSettings": { "title": "Conta admin", "description": "Atualize as credenciais usadas para entrar no PicPeak.", - "username": "Nome de usuario", + "username": "Nome de usuário", "usernamePlaceholder": "Admin", "email": "Email", - "emailPlaceholder": "admin@exemplo.com", + "emailPlaceholder": "admin@example.com", "updateButton": "Atualizar perfil" }, - "profileUpdateError": "Nao foi possivel atualizar o perfil admin. Por favor, tente novamente.", + "profileUpdateError": "Não foi possível atualizar o perfil admin. Por favor, tente novamente.", "loadingDashboard": "Carregando painel...", "activeEvents": "Eventos Ativos", "expiringSoon": "Expirando em Breve", - "next7Days": "Proximos 7 dias", - "totalViews": "Total de Visualizacoes", + "next7Days": "Próximos 7 dias", + "totalViews": "Total de Visualizações", "downloads": "Downloads", "percentFromLastWeek": "{{percent}}% da semana passada", - "dashboardSubtitle": "Bem-vindo de volta! Veja o que esta acontecendo com suas galerias.", + "dashboardSubtitle": "Bem-vindo de volta! Veja o que está acontecendo com suas galerias.", "eventsExpiringSoon": "Eventos Expirando em Breve", - "noEventsExpiring": "Nenhum evento expirando nos proximos 7 dias", + "noEventsExpiring": "Nenhum evento expirando nos próximos 7 dias", "daysLeft": "{{count}} dia restante", "daysLeft_plural": "{{count}} dias restantes", "viewAllExpiringEvents": "Ver todos os {{count}} eventos expirando", "noRecentActivity": "Nenhuma atividade recente", - "viewAllActivity": "Ver toda atividade", - "quickActions": "Acoes Rapidas", + "viewAllActivity": "Ver toda a atividade", + "quickActions": "Ações Rápidas", "viewArchives": "Ver Arquivos", - "analytics": "Analiticos", + "analytics": "Análises", "activities": { "event_created": "Novo evento criado: {{eventName}}", "photos_uploaded": "{{count}} fotos enviadas para {{eventName}}", "event_archived": "Evento arquivado: {{eventName}}", "archive_restored": "Arquivo restaurado: {{eventName}}", - "archive_deleted": "Arquivo excluido: {{eventName}}", + "archive_deleted": "Arquivo excluído: {{eventName}}", "archive_downloaded": "Arquivo baixado: {{eventName}}", - "email_config_updated": "Configuracao de email atualizada", - "email_template_updated": "Template de email atualizado: {{template}}", - "branding_updated": "Configuracoes de marca atualizadas", - "theme_updated": "Configuracoes de tema atualizadas", + "email_config_updated": "Configuração de email atualizada", + "email_template_updated": "Modelo de email atualizado: {{template}}", + "branding_updated": "Configurações de identidade visual atualizadas", + "theme_updated": "Configurações de tema atualizadas", "bulk_download": "{{count}} fotos baixadas de {{eventName}}", "gallery_password_entry": "Senha inserida para {{eventName}}", - "expiration_warning_viewed": "Aviso de expiracao visualizado para {{eventName}}", - "feedback_settings_updated": "Configuracoes de feedback atualizadas", + "expiration_warning_viewed": "Aviso de expiração visualizado para {{eventName}}", + "feedback_settings_updated": "Configurações de feedback atualizadas", "feedback_moderated": "Feedback moderado", - "feedback_deleted": "Feedback excluido", + "feedback_deleted": "Feedback excluído", "photo_like": "Foto curtida em {{eventName}}", "photo_favorite": "Foto favoritada em {{eventName}}", "photo_rating": "Foto avaliada em {{eventName}}", "photo_comment": "Foto comentada em {{eventName}}", - "guest_feedback_like": "Convidado curtiu uma foto em {{eventName}}", - "guest_feedback_favorite": "Convidado favoritou uma foto em {{eventName}}", - "guest_feedback_rating": "Convidado avaliou uma foto em {{eventName}}", - "guest_feedback_comment": "Convidado comentou uma foto em {{eventName}}", - "word_filter_added": "Filtro de palavra adicionado", - "external_import_completed": "Importacao de midia externa concluida ({{imported}} importados, {{skipped}} ignorados)", - "bulk_archive_completed": "Arquivamento em massa concluido", + "guest_feedback_like": "Visitante curtiu uma foto em {{eventName}}", + "guest_feedback_favorite": "Visitante favoritou uma foto em {{eventName}}", + "guest_feedback_rating": "Visitante avaliou uma foto em {{eventName}}", + "guest_feedback_comment": "Visitante comentou em uma foto em {{eventName}}", + "word_filter_added": "Filtro de palavras adicionado", + "external_import_completed": "Importação de mídia externa concluída ({{imported}} importadas, {{skipped}} ignoradas)", + "bulk_archive_completed": "Arquivamento em massa concluído", "event_activated": "Evento ativado: {{eventName}}", "event_deactivated": "Evento desativado: {{eventName}}", - "photo_deleted": "Foto excluida de {{eventName}}", - "photos_bulk_deleted": "{{count}} fotos excluidas de {{eventName}}", - "settings_updated": "Configuracoes atualizadas", + "photo_deleted": "Foto excluída de {{eventName}}", + "photos_bulk_deleted": "{{count}} fotos excluídas de {{eventName}}", + "settings_updated": "Configurações atualizadas", "event_updated": "Evento atualizado: {{eventName}}", "event_renamed": "Evento renomeado: {{eventName}}", - "event_deleted": "Evento excluido: {{eventName}}", + "event_deleted": "Evento excluído: {{eventName}}", "password_changed": "Senha alterada", - "email_resent": "Email de criacao reenviado para: {{eventName}}", + "email_resent": "Email de criação reenviado para: {{eventName}}", "category_created": "Categoria criada: {{categoryName}}", "category_updated": "Categoria atualizada: {{categoryName}}", - "category_deleted": "Categoria excluida: {{categoryName}}", - "general_settings_updated": "Configuracoes gerais atualizadas", + "category_deleted": "Categoria excluída: {{categoryName}}", + "general_settings_updated": "Configurações gerais atualizadas", "favicon_uploaded": "Favicon enviado", - "analytics_settings_updated": "Configuracoes de analiticos atualizadas", - "cms_page_updated": "Pagina CMS atualizada: {{page}}", - "security_settings_updated": "Configuracoes de seguranca atualizadas", + "analytics_settings_updated": "Configurações de análise atualizadas", + "cms_page_updated": "Página CMS atualizada: {{page}}", + "security_settings_updated": "Configurações de segurança atualizadas", "password_reset": "Senha redefinida para: {{eventName}}", - "admin_logout": "Admin {{actorName}} fez logout", + "admin_logout": "Admin {{actorName}} encerrou a sessão", "system_activity": "Atividade do sistema: {{type}}", "unknown": "Atividade desconhecida" }, - "userManagement": "Gerenciamento de Usuarios", - "inviteUser": "Convidar Usuario", + "userManagement": "Gerenciamento de Usuários", + "inviteUser": "Convidar Usuário", "pendingInvitations": "Convites Pendentes", "roles": { "super_admin": "Super Admin", @@ -1440,225 +1440,232 @@ "inactive": "Inativo" }, "inviteForm": { - "email": "Endereco de Email", - "role": "Funcao", + "email": "Endereço de Email", + "role": "Função", "send": "Enviar Convite" }, "acceptInvite": { "title": "Aceitar Convite de Admin", - "username": "Escolha um Nome de Usuario", + "username": "Escolha um Nome de Usuário", "password": "Criar Senha", "submit": "Criar Conta" + }, + "photos": { + "hidden": "Oculta", + "hideSelected": "Ocultar", + "showSelected": "Exibir", + "hiddenSuccess": "Fotos ocultadas dos visitantes", + "visibleSuccess": "Fotos agora visíveis aos visitantes" } }, "permissions": { - "insufficient": "Voce nao tem permissao para realizar esta acao", - "viewOnly": "Apenas Visualizacao" + "insufficient": "Você não tem permissão para realizar esta ação", + "viewOnly": "Somente Visualização" }, "acceptInvitation": { "title": "Aceitar Convite", - "subtitle": "Crie sua conta de admin", + "subtitle": "Crie sua conta admin", "validating": "Validando convite...", - "invalidToken": "Convite Invalido", - "invalidTokenMessage": "Este link de convite e invalido ou expirou. Por favor, entre em contato com seu administrador para um novo convite.", + "invalidToken": "Convite Inválido", + "invalidTokenMessage": "Este link de convite é inválido ou expirou. Entre em contato com seu administrador para um novo convite.", "expiredToken": "Convite Expirado", - "expiredTokenMessage": "Este convite expirou. Por favor, solicite um novo convite ao seu administrador.", - "alreadyUsed": "Convite Ja Utilizado", - "alreadyUsedMessage": "Este convite ja foi usado para criar uma conta.", - "invitedAs": "Voce foi convidado como", - "expiresAt": "O convite expira", - "usernameLabel": "Nome de Usuario", - "usernamePlaceholder": "Escolha um nome de usuario", - "usernameHelp": "3-50 caracteres, apenas letras, numeros, sublinhados e hifens", + "expiredTokenMessage": "Este convite expirou. Solicite um novo convite ao seu administrador.", + "alreadyUsed": "Convite Já Utilizado", + "alreadyUsedMessage": "Este convite já foi usado para criar uma conta.", + "invitedAs": "Você foi convidado como", + "expiresAt": "O convite expira em", + "usernameLabel": "Nome de usuário", + "usernamePlaceholder": "Escolha um nome de usuário", + "usernameHelp": "3-50 caracteres, apenas letras, números, sublinhados e hífens", "passwordLabel": "Senha", "passwordPlaceholder": "Crie uma senha forte", "confirmPasswordLabel": "Confirmar Senha", "confirmPasswordPlaceholder": "Confirme sua senha", - "passwordStrength": "Forca da senha", + "passwordStrength": "Força da senha", "requirements": { "title": "Requisitos da senha:", "minLength": "Pelo menos 12 caracteres", - "uppercase": "Pelo menos uma letra maiuscula", - "lowercase": "Pelo menos uma letra minuscula", - "number": "Pelo menos um numero", + "uppercase": "Pelo menos uma letra maiúscula", + "lowercase": "Pelo menos uma letra minúscula", + "number": "Pelo menos um número", "special": "Pelo menos um caractere especial" }, "strength": { "weak": "Fraca", - "fair": "Razoavel", + "fair": "Razoável", "good": "Boa", "strong": "Forte" }, "createAccount": "Criar Conta", "creating": "Criando conta...", "success": "Conta Criada!", - "successMessage": "Sua conta foi criada com sucesso. Voce ja pode fazer login com suas credenciais.", - "redirecting": "Redirecionando para login em {{seconds}}...", + "successMessage": "Sua conta foi criada com sucesso. Agora você pode entrar com suas credenciais.", + "redirecting": "Redirecionando para o login em {{seconds}}...", "goToLogin": "Ir para Login", - "contactAdminMessage": "Por favor, entre em contato com seu administrador para solicitar um novo convite.", - "passwordsMatch": "Senhas conferem", - "alreadyHaveAccount": "Ja possui uma conta?", + "contactAdminMessage": "Entre em contato com seu administrador para solicitar um novo convite.", + "passwordsMatch": "As senhas coincidem", + "alreadyHaveAccount": "Já tem uma conta?", "signIn": "Entrar", "errors": { - "usernameRequired": "Nome de usuario e obrigatorio", - "usernameTooShort": "Nome de usuario deve ter pelo menos 3 caracteres", - "usernameTooLong": "Nome de usuario deve ter no maximo 50 caracteres", - "usernameInvalid": "Nome de usuario pode conter apenas letras, numeros, sublinhados e hifens", - "passwordRequired": "Senha e obrigatoria", - "passwordTooShort": "Senha deve ter pelo menos 12 caracteres", - "passwordsDoNotMatch": "Senhas nao conferem", + "usernameRequired": "Nome de usuário é obrigatório", + "usernameTooShort": "O nome de usuário deve ter pelo menos 3 caracteres", + "usernameTooLong": "O nome de usuário deve ter no máximo 50 caracteres", + "usernameInvalid": "O nome de usuário só pode conter letras, números, sublinhados e hífens", + "passwordRequired": "Senha é obrigatória", + "passwordTooShort": "A senha deve ter pelo menos 12 caracteres", + "passwordsDoNotMatch": "As senhas não coincidem", "confirmPasswordRequired": "Por favor, confirme sua senha", - "usernameTaken": "Este nome de usuario ja esta em uso", - "emailTaken": "Uma conta com este email ja existe", + "usernameTaken": "Este nome de usuário já está em uso", + "emailTaken": "Já existe uma conta com este email", "genericError": "Falha ao criar conta. Por favor, tente novamente." } }, "errors": { - "notFound": "Nao Encontrado", - "galleryNotFound": "Galeria Nao Encontrada", - "galleryNotFoundMessage": "Esta galeria nao existe ou foi removida.", + "notFound": "Não Encontrado", + "galleryNotFound": "Galeria Não Encontrada", + "galleryNotFoundMessage": "Esta galeria não existe ou foi removida.", "galleryArchived": "Galeria Arquivada", - "galleryArchivedMessage": "Esta galeria foi arquivada e nao esta mais acessivel. Por favor, entre em contato com o organizador do evento se precisar de acesso a estas fotos.", - "unauthorized": "Nao Autorizado", - "forbidden": "Proibido", - "serverError": "Erro do Servidor", + "galleryArchivedMessage": "Esta galeria foi arquivada e não está mais acessível. Entre em contato com o organizador do evento se precisar de acesso a estas fotos.", + "unauthorized": "Não Autorizado", + "forbidden": "Acesso Negado", + "serverError": "Erro no Servidor", "somethingWentWrong": "Algo deu errado", "tryAgainLater": "Por favor, tente novamente mais tarde", - "refreshPage": "Atualizar Pagina", + "refreshPage": "Recarregar Página", "oopsSomethingWentWrong": "Ops! Algo deu errado", - "unexpectedError": "Encontramos um erro inesperado. Nao se preocupe, seus dados estao seguros.", - "goToHomepage": "Ir para a Pagina Inicial", + "unexpectedError": "Encontramos um erro inesperado. Não se preocupe, seus dados estão seguros.", + "goToHomepage": "Ir para a Página Inicial", "errorDetails": "Detalhes do Erro", - "requiredFields": "Por favor, preencha todos os campos obrigatorios", - "enterTestEmail": "Por favor, digite um endereco de email de teste", + "requiredFields": "Por favor, preencha todos os campos obrigatórios", + "enterTestEmail": "Por favor, insira um endereço de email de teste", "failedToCreateEvent": "Falha ao criar evento", "eventCreationFailed": "Falha ao criar evento", - "networkError": "Erro de rede. Por favor, verifique sua conexao e tente novamente.", - "sessionExpired": "Sessao expirada. Por favor, faca login novamente." + "networkError": "Erro de rede. Por favor, verifique sua conexão e tente novamente.", + "sessionExpired": "Sessão expirada. Por favor, faça login novamente." }, "validation": { - "eventNameRequired": "Nome do evento e obrigatorio", - "hostEmailRequired": "Email do cliente e obrigatorio", - "hostNameRequired": "Nome do cliente e obrigatorio", - "adminEmailRequired": "Email do admin e obrigatorio", - "invalidEmailFormat": "Formato de email invalido", - "passwordRequired": "Senha e obrigatoria", - "passwordMinLength": "Senha deve ter pelo menos 6 caracteres", - "passwordsDoNotMatch": "Senhas nao conferem", - "passwordSecurityRequirements": "Senha nao atende aos requisitos de seguranca", - "expirationRange": "Expiracao deve ser entre 1 e 365 dias" + "eventNameRequired": "O nome do evento é obrigatório", + "hostEmailRequired": "O email do cliente é obrigatório", + "hostNameRequired": "O nome do cliente é obrigatório", + "adminEmailRequired": "O email do admin é obrigatório", + "invalidEmailFormat": "Formato de email inválido", + "passwordRequired": "Senha é obrigatória", + "passwordMinLength": "A senha deve ter pelo menos 6 caracteres", + "passwordsDoNotMatch": "As senhas não coincidem", + "passwordSecurityRequirements": "A senha não atende aos requisitos de segurança", + "expirationRange": "A expiração deve ser entre 1 e 365 dias" }, "legal": { "impressum": "Aviso Legal", - "datenschutz": "Politica de Privacidade", - "termsOfService": "Termos de Servico", - "cookiePolicy": "Politica de Cookies" + "datenschutz": "Política de Privacidade", + "termsOfService": "Termos de Serviço", + "cookiePolicy": "Política de Cookies" }, "toast": { - "saveSuccess": "Alteracoes salvas com sucesso", - "saveError": "Falha ao salvar alteracoes", - "deleteSuccess": "Excluido com sucesso", + "saveSuccess": "Alterações salvas com sucesso", + "saveError": "Falha ao salvar alterações", + "deleteSuccess": "Excluído com sucesso", "deleteError": "Falha ao excluir", - "uploadSuccess": "Upload concluido com sucesso", + "uploadSuccess": "Upload concluído com sucesso", "uploadError": "Falha no upload", "loginSuccess": "Login realizado com sucesso", "loginError": "Falha no login", "passwordChanged": "Senha alterada com sucesso", - "linkCopied": "Link copiado para a area de transferencia", + "linkCopied": "Link copiado para a área de transferência", "eventCreated": "Evento criado com sucesso", "eventUpdated": "Evento atualizado com sucesso", "eventArchived": "Evento arquivado com sucesso", - "settingsSaved": "Configuracoes salvas com sucesso", + "settingsSaved": "Configurações salvas com sucesso", "themeUpdated": "Tema atualizado com sucesso", - "brandingUpdated": "Marca atualizada com sucesso", + "brandingUpdated": "Identidade visual atualizada com sucesso", "categoryAdded": "Categoria adicionada com sucesso", - "categoryDeleted": "Categoria excluida com sucesso", + "categoryDeleted": "Categoria excluída com sucesso", "categoryUpdated": "Categoria atualizada com sucesso", - "emailConfigSaved": "Configuracao de email salva com sucesso", + "emailConfigSaved": "Configuração de email salva com sucesso", "testEmailSent": "Email de teste enviado com sucesso", - "pageUpdated": "Pagina atualizada com sucesso", + "pageUpdated": "Página atualizada com sucesso", "archiveRestored": "Arquivo restaurado com sucesso", - "archiveDeleted": "Arquivo excluido permanentemente" + "archiveDeleted": "Arquivo excluído permanentemente" }, "email": { - "title": "Configuracao de Email", - "subtitle": "Configure as configuracoes de email para notificacoes", - "loadingSettings": "Carregando configuracoes de email...", - "smtpConfiguration": "Configuracao SMTP", + "title": "Configuração de Email", + "subtitle": "Configure as definições de email para notificações", + "loadingSettings": "Carregando configurações de email...", + "smtpConfiguration": "Configuração SMTP", "smtpHost": "Host SMTP", - "smtpHostHelp": "Nome do host do seu servidor de email", + "smtpHostHelp": "O nome do host do seu servidor de email", "smtpPort": "Porta SMTP", - "smtpPortHelp": "Geralmente 587 para TLS, 465 para SSL, 25 para nao criptografado", + "smtpPortHelp": "Geralmente 587 para TLS, 465 para SSL, 25 para sem criptografia", "smtpSecure": "Usar SSL/TLS", - "smtpSecureHelp": "Ativar para transmissao segura de email", - "smtpUsername": "Usuario SMTP", - "smtpUsernameHelp": "Nome de usuario da sua conta de email", + "smtpSecureHelp": "Ative para transmissão segura de email", + "smtpUsername": "Usuário SMTP", + "smtpUsernameHelp": "O nome de usuário da sua conta de email", "smtpPassword": "Senha SMTP", - "smtpPasswordHelp": "Senha da sua conta de email", + "smtpPasswordHelp": "A senha da sua conta de email", "fromDetails": "Detalhes do Remetente", "fromEmail": "Email do Remetente", - "fromEmailHelp": "Endereco de email que aparece como remetente", + "fromEmailHelp": "Endereço de email que aparece como remetente", "fromName": "Nome do Remetente", "fromNameHelp": "Nome que aparece como remetente", - "testConfiguration": "Testar Configuracao", - "testEmail": "Endereco de Email de Teste", - "testEmailHelp": "Envie um email de teste para verificar as configuracoes", + "testConfiguration": "Testar Configuração", + "testEmail": "Endereço de Email de Teste", + "testEmailHelp": "Envie um email de teste para verificar as configurações", "sendTestEmail": "Enviar Email de Teste", - "saveConfiguration": "Salvar Configuracao", - "emailTemplates": "Templates de Email", - "templateVariables": "Variaveis Disponiveis", - "previewTemplate": "Visualizar Template", - "smtpSettings": "Configuracoes SMTP", + "saveConfiguration": "Salvar Configuração", + "emailTemplates": "Modelos de Email", + "templateVariables": "Variáveis Disponíveis", + "previewTemplate": "Pré-visualizar Modelo", + "smtpSettings": "Configurações SMTP", "testEmailSuccess": "Email de teste enviado com sucesso", - "saveSmtpSettings": "Salvar Configuracoes SMTP", + "saveSmtpSettings": "Salvar Configurações SMTP", "testEmailSection": "Email de Teste", "beforeTesting": "Antes de testar:", - "saveSmtpFirst": "Salve suas configuracoes SMTP primeiro", - "ensureFirewall": "Certifique-se de que seu firewall permite SMTP de saida", - "gmailAppPassword": "Para Gmail, use uma senha especifica do aplicativo", - "testEmailAddressLabel": "Endereco de Email de Teste", + "saveSmtpFirst": "Salve suas configurações SMTP primeiro", + "ensureFirewall": "Certifique-se de que seu firewall permite SMTP de saída", + "gmailAppPassword": "Para Gmail, use uma senha específica de aplicativo", + "testEmailAddressLabel": "Endereço de Email de Teste", "sendTestEmailButton": "Enviar Email de Teste", - "commonSmtpSettings": "Configuracoes SMTP Comuns:", - "editTemplate": "Editar Template", - "templateName": "Nome do Template", + "commonSmtpSettings": "Configurações SMTP Comuns:", + "editTemplate": "Editar Modelo", + "templateName": "Nome do Modelo", "subjectLine": "Linha de Assunto", "emailBody": "Corpo do Email", - "preview": "Visualizar", + "preview": "Pré-visualizar", "save": "Salvar", - "saveChanges": "Salvar Alteracoes", - "templates": "Templates", - "variableHelp": "Use estas variaveis no seu template. Elas serao substituidas por valores reais quando os emails forem enviados.", + "saveChanges": "Salvar Alterações", + "templates": "Modelos", + "variableHelp": "Use estas variáveis no seu modelo. Elas serão substituídas pelos valores reais quando os emails forem enviados.", "port": "Porta", - "security": "Seguranca", - "username": "Nome de usuario", + "security": "Segurança", + "username": "Usuário", "password": "Senha", "enterPassword": "Digite a senha", - "required": "obrigatorio", + "required": "obrigatório", "ignoreSslErrors": "Ignorar erros de certificado SSL/TLS", - "ignoreSslWarning": "Aviso: Desabilitar verificacao de certificado torna a conexao vulneravel a ataques man-in-the-middle. Ative somente se voce confia no servidor SMTP e entende as implicacoes de seguranca.", - "brandingTitle": "Identidade Visual do E-mail", - "brandingDescription": "Personalize as cores dos modelos de e-mail. As alteracoes se aplicam ao cabecalho, botoes, links e fundo do rodape.", - "primaryColor": "Cor Primaria", - "primaryColorHint": "Usada no cabecalho, botoes e links", - "secondaryColor": "Fundo do Rodape", - "secondaryColorHint": "Usada no fundo da secao de rodape", - "saveEmailColors": "Salvar Cores do E-mail", + "ignoreSslWarning": "Aviso: Desativar a verificação de certificado torna a conexão vulnerável a ataques man-in-the-middle. Ative isso somente se você confia no servidor SMTP e compreende as implicações de segurança.", + "brandingTitle": "Identidade Visual do Email", + "brandingDescription": "Personalize as cores usadas nos modelos de email. As alterações se aplicam à barra de cabeçalho, botões, links e fundo do rodapé.", + "primaryColor": "Cor Primária", + "primaryColorHint": "Usada para cabeçalho, botões e links", + "secondaryColor": "Fundo do Rodapé", + "secondaryColorHint": "Usada para o fundo da seção de rodapé", + "saveEmailColors": "Salvar Cores do Email", "editor": { "bold": "Negrito", - "italic": "Italico", + "italic": "Itálico", "bulletList": "Lista com Marcadores", "numberedList": "Lista Numerada", - "blockquote": "Citacao", + "blockquote": "Citação", "link": "Adicionar Link", "horizontalRule": "Linha Horizontal", - "alignLeft": "Alinhar a Esquerda", + "alignLeft": "Alinhar à Esquerda", "alignCenter": "Centralizar", - "alignRight": "Alinhar a Direita", - "clearFormatting": "Limpar Formatacao", + "alignRight": "Alinhar à Direita", + "clearFormatting": "Limpar Formatação", "undo": "Desfazer", "redo": "Refazer", - "insertVariable": "Inserir Variavel", - "sourceMode": "Codigo", + "insertVariable": "Inserir Variável", + "sourceMode": "Código", "visualMode": "Visual", "enterUrl": "Digite a URL...", "addLink": "Adicionar", @@ -1666,92 +1673,92 @@ } }, "cms": { - "title": "Paginas CMS", - "subtitle": "Gerenciar paginas legais e informativas", - "loadingPages": "Carregando paginas...", - "pages": "Paginas", - "previewLinks": "Links de Visualizacao", - "englishVersion": "Versao em Ingles", - "germanVersion": "Versao em Alemao", + "title": "Páginas CMS", + "subtitle": "Gerencie páginas legais e informativas", + "loadingPages": "Carregando páginas...", + "pages": "Páginas", + "previewLinks": "Links de Visualização", + "englishVersion": "Versão em Inglês", + "germanVersion": "Versão em Alemão", "editPage": "Editar {{page}}", - "pageTitle": "Titulo da Pagina", - "pageContent": "Conteudo da Pagina", - "pageTitlePlaceholder": "Digite o titulo da pagina...", - "saveChanges": "Salvar Alteracoes", - "lastUpdated": "Ultima atualizacao:", + "pageTitle": "Título da Página", + "pageContent": "Conteúdo da Página", + "pageTitlePlaceholder": "Digite o título da página...", + "saveChanges": "Salvar Alterações", + "lastUpdated": "Última atualização:", "impressum": "Aviso Legal", - "datenschutz": "Politica de Privacidade", - "pageUpdated": "Pagina atualizada com sucesso" + "datenschutz": "Política de Privacidade", + "pageUpdated": "Página atualizada com sucesso" }, "eventTypes": { "title": "Tipos de Evento", - "subtitle": "Personalize tipos de evento e seus temas padrao", + "subtitle": "Personalize os tipos de evento e seus temas padrão", "loading": "Carregando tipos de evento...", "loadError": "Falha ao carregar tipos de evento", "createNew": "Novo Tipo de Evento", "edit": "Editar Tipo de Evento", "created": "Tipo de evento criado com sucesso", "updated": "Tipo de evento atualizado com sucesso", - "deleted": "Tipo de evento excluido com sucesso", + "deleted": "Tipo de evento excluído com sucesso", "createError": "Falha ao criar tipo de evento", "updateError": "Falha ao atualizar tipo de evento", "deleteError": "Falha ao excluir tipo de evento", - "searchPlaceholder": "Buscar tipos de evento...", + "searchPlaceholder": "Pesquisar tipos de evento...", "showInactive": "Mostrar inativos", "noResults": "Nenhum tipo de evento encontrado", "empty": "Nenhum tipo de evento ainda", "system": "Sistema", "table": { "type": "Tipo", - "slugPrefix": "Prefixo da URL", - "theme": "Tema Padrao", + "slugPrefix": "Prefixo de URL", + "theme": "Tema Padrão", "status": "Status", - "actions": "Acoes" + "actions": "Ações" }, "form": { - "name": "Nome de Exibicao", - "namePlaceholder": "ex., Ensaio Familiar", - "slugPrefix": "Prefixo da URL", - "slugPrefixPlaceholder": "ex., familia", + "name": "Nome de Exibição", + "namePlaceholder": "ex.: Ensaio de Família", + "slugPrefix": "Prefixo de URL", + "slugPrefixPlaceholder": "ex.: familia", "slugPreview": "URL de exemplo:", - "emoji": "Icone", - "themePreset": "Tema Padrao", - "isActive": "Ativo (visivel na criacao de eventos)" + "emoji": "Ícone", + "themePreset": "Tema Padrão", + "isActive": "Ativo (visível na criação de eventos)" }, "validation": { - "slugFormat": "Apenas letras, numeros e hifens sao permitidos" + "slugFormat": "Apenas letras, números e hifens são permitidos" }, "slugInfo": { - "title": "Info do Prefixo da URL:", - "description": "O prefixo da URL e usado para gerar URLs de galeria. Por exemplo, um tipo de evento com prefixo \"familia\" criara URLs como: familia-silva-familia-2025-01-22" + "title": "Informações sobre o Prefixo de URL:", + "description": "O prefixo de URL é usado para gerar as URLs das galerias. Por exemplo, um tipo de evento com o prefixo \"familia\" criará URLs como: familia-silva-familia-2025-01-22" }, "deleteConfirm": { "title": "Excluir Tipo de Evento", "message": "Tem certeza que deseja excluir", - "warning": "Esta acao nao pode ser desfeita. Certifique-se de que nenhum evento esta usando este tipo." + "warning": "Esta ação não pode ser desfeita. Certifique-se de que nenhum evento está usando este tipo." } }, "backup": { "external": { "warning": { - "title": "Midia externa excluida", - "body": "Esta instalacao referencia fotos de /external-media. Estes originais sao excluidos dos backups. Miniaturas e banco de dados ainda sao salvos." + "title": "Mídia externa excluída", + "body": "Esta instalação referencia fotos de /external-media. Esses originais estão excluídos dos backups. Miniaturas e banco de dados ainda são incluídos no backup." } }, "title": "Gerenciamento de Backup", "subtitle": "Gerencie backups do sistema, configure backups automatizados e restaure a partir de backups anteriores.", "tabs": { "dashboard": "Painel", - "configuration": "Configuracao", - "history": "Historico de Backup", + "configuration": "Configuração", + "history": "Histórico de Backups", "restore": "Restaurar" }, "status": { "inProgress": "Backup em andamento...", - "lastBackup": "Ultimo backup", + "lastBackup": "Último backup", "noBackups": "Nenhum backup encontrado", - "nextBackup": "Proximo backup", - "notScheduled": "Nao agendado", + "nextBackup": "Próximo backup", + "notScheduled": "Não agendado", "enabled": "Ativado", "disabled": "Desativado" }, @@ -1759,38 +1766,38 @@ "runBackupNow": "Executar Backup Agora", "starting": "Iniciando...", "running": "Executando...", - "testConnection": "Testar Conexao", - "save": "Salvar Configuracao", + "testConnection": "Testar Conexão", + "save": "Salvar Configuração", "delete": "Excluir", "view": "Ver Detalhes", "download": "Baixar", "refresh": "Atualizar" }, "dashboard": { - "backupHealth": "Saude do Backup", + "backupHealth": "Integridade do Backup", "healthStatus": { "excellent": "Excelente", "good": "Bom", - "warning": "Alerta", - "critical": "Critico" + "warning": "Atenção", + "critical": "Crítico" }, "health": { - "title": "Saude do Backup" + "title": "Integridade do Backup" }, "healthMessages": { "noBackups": "Nenhum backup encontrado", - "lastBackupFailed": "Ultimo backup falhou", - "upToDate": "Backup esta atualizado", - "recent": "Backup e recente", - "gettingOld": "Backup esta ficando antigo", - "outdated": "Backup esta desatualizado" + "lastBackupFailed": "Último backup falhou", + "upToDate": "Backup está atualizado", + "recent": "Backup é recente", + "gettingOld": "Backup está ficando antigo", + "outdated": "Backup está desatualizado" }, "stats": { "totalBackups": "Total de Backups", "backupSize": "Tamanho do Backup", - "lastDuration": "Ultima Duracao", + "lastDuration": "Duração do Último", "backupStatus": "Status do Backup", - "last": "Ultimo", + "last": "Último", "files": "arquivos", "minutes": "{{count}}m", "active": "Ativo", @@ -1801,8 +1808,8 @@ "title": "Atividade Recente de Backup" }, "notConfigured": { - "title": "Backup Nao Configurado", - "message": "Por favor, configure as configuracoes de backup na aba Configuracao antes de executar backups." + "title": "Backup Não Configurado", + "message": "Por favor, configure as definições de backup na aba Configuração antes de executar backups." }, "actions": { "runBackupNow": "Executar Backup Agora", @@ -1814,12 +1821,12 @@ "photos": "Fotos", "archives": "Arquivos", "systemFiles": "Arquivos do Sistema", - "included": "Incluido", - "excluded": "Excluido", + "included": "Incluído", + "excluded": "Excluído", "optional": "Opcional" }, "storageDestination": "Destino de Armazenamento", - "nextScheduledBackup": "Proximo Backup Agendado", + "nextScheduledBackup": "Próximo Backup Agendado", "backupType": "Backup {{type}}", "noDestinationSet": "Nenhum destino definido" }, @@ -1837,40 +1844,40 @@ "description": "Sincronizar backups para um servidor remoto via SSH/Rsync" }, "s3": { - "name": "Armazenamento Compativel com S3", - "description": "Armazenar backups no Amazon S3 ou armazenamento de objetos compativel" + "name": "Armazenamento S3 Compatível", + "description": "Armazenar backups no Amazon S3 ou armazenamento de objetos compatível" } }, "fields": { "destinationPath": "Caminho de Destino", - "destinationPathHelp": "Caminho do diretorio local para armazenar backups", + "destinationPathHelp": "Caminho do diretório local para armazenar backups", "destinationPathPlaceholder": "/caminho/para/diretorio/backup", "rsyncHost": "Host Remoto", - "rsyncHostHelp": "Nome do host ou endereco IP SSH", + "rsyncHostHelp": "Nome do host SSH ou endereço IP", "rsyncHostPlaceholder": "backup.exemplo.com", - "rsyncUser": "Usuario SSH", - "rsyncUserHelp": "Nome de usuario para conexao SSH", + "rsyncUser": "Usuário SSH", + "rsyncUserHelp": "Nome de usuário para conexão SSH", "rsyncUserPlaceholder": "usuario-backup", "rsyncPath": "Caminho Remoto", - "rsyncPathHelp": "Caminho do diretorio no servidor remoto", + "rsyncPathHelp": "Caminho do diretório no servidor remoto", "rsyncPathPlaceholder": "/home/backup/compartilhamento-fotos", "rsyncSshKey": "Chave Privada SSH", - "rsyncSshKeyHelp": "Chave privada SSH para autenticacao (opcional)", + "rsyncSshKeyHelp": "Chave privada SSH para autenticação (opcional)", "rsyncSshKeyPlaceholder": "-----BEGIN RSA PRIVATE KEY-----", "s3Endpoint": "Endpoint S3", - "s3EndpointHelp": "Endpoint da API S3 (ex., s3.amazonaws.com)", + "s3EndpointHelp": "Endpoint da API S3 (ex.: s3.amazonaws.com)", "s3EndpointPlaceholder": "https://s3.amazonaws.com", "s3Bucket": "Nome do Bucket", "s3BucketHelp": "Bucket S3 para armazenar backups", - "s3BucketPlaceholder": "meu-bucket-backup", + "s3BucketPlaceholder": "meu-bucket-de-backup", "s3AccessKey": "ID da Chave de Acesso", "s3AccessKeyHelp": "ID da chave de acesso AWS/S3", - "s3AccessKeyPlaceholder": "AKIAIOSFODNN7EXEMPLO", + "s3AccessKeyPlaceholder": "AKIAIOSFODNN7EXAMPLE", "s3SecretKey": "Chave de Acesso Secreta", "s3SecretKeyHelp": "Chave de acesso secreta AWS/S3", - "s3SecretKeyPlaceholder": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYCHAVEEXEMPLO", - "s3Region": "Regiao", - "s3RegionHelp": "Regiao S3 (ex., us-east-1)", + "s3SecretKeyPlaceholder": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "s3Region": "Região", + "s3RegionHelp": "Região S3 (ex.: us-east-1)", "s3RegionPlaceholder": "us-east-1" }, "schedule": { @@ -1878,66 +1885,66 @@ "scheduleType": "Tipo de Agendamento", "scheduleOptions": { "hourly": "A cada hora", - "daily": "Diario", - "weekly": "Semanal", - "custom": "Expressao cron personalizada" + "daily": "Diariamente", + "weekly": "Semanalmente", + "custom": "Expressão cron personalizada" }, "options": { "hourly": "A cada hora", - "daily": "Diario", - "weekly": "Semanal", - "custom": "Expressao cron personalizada" + "daily": "Diariamente", + "weekly": "Semanalmente", + "custom": "Expressão cron personalizada" }, - "customCron": "Expressao Cron", - "customCronHelp": "Digite uma expressao cron valida (ex., 0 3 * * *)", - "retention": "Periodo de Retencao", + "customCron": "Expressão Cron", + "customCronHelp": "Digite uma expressão cron válida (ex.: 0 3 * * *)", + "retention": "Período de Retenção", "retentionDays": "Manter backups por", - "retentionHelp": "dias (backups mais antigos serao excluidos automaticamente)" + "retentionHelp": "dias (backups mais antigos serão excluídos automaticamente)" }, "whatToBackup": { - "title": "O que Fazer Backup", + "title": "O Que Incluir no Backup", "database": "Banco de Dados", - "databaseHelp": "Todos os dados de eventos, configuracoes e configuracoes", + "databaseHelp": "Todos os dados de eventos, configurações e definições", "photos": "Fotos", "photosHelp": "Todas as fotos enviadas em galerias ativas", "archives": "Arquivos", "archivesHelp": "Arquivos ZIP de eventos arquivados", "thumbnails": "Miniaturas", "thumbnailsHelp": "Imagens de miniatura geradas (podem ser recriadas)", - "tempFiles": "Arquivos Temporarios", - "tempFilesHelp": "Arquivos temporarios de upload e processamento" + "tempFiles": "Arquivos Temporários", + "tempFilesHelp": "Arquivos temporários de upload e processamento" }, "advancedOptions": { - "title": "Opcoes Avancadas", - "compression": "Ativar Compressao", - "compressionHelp": "Comprimir arquivos de backup para economizar espaco", + "title": "Opções Avançadas", + "compression": "Ativar Compressão", + "compressionHelp": "Comprimir arquivos de backup para economizar espaço de armazenamento", "encryption": "Ativar Criptografia", - "encryptionHelp": "Criptografar backups para seguranca adicional", - "encryptionPassphrase": "Frase de Criptografia", - "encryptionPassphraseHelp": "Frase forte para criptografia do backup", - "confirmPassphrase": "Confirmar Frase", - "passphrasesDontMatch": "As frases nao conferem" + "encryptionHelp": "Criptografar backups para segurança adicional", + "encryptionPassphrase": "Senha de Criptografia", + "encryptionPassphraseHelp": "Senha forte para criptografia de backup", + "confirmPassphrase": "Confirmar Senha", + "passphrasesDontMatch": "As senhas não coincidem" }, "validation": { - "requiredFields": "Por favor, preencha todos os campos obrigatorios", - "invalidCron": "Expressao cron invalida", - "connectionTestFailed": "Teste de conexao falhou", - "connectionTestSuccess": "Teste de conexao bem-sucedido!" + "requiredFields": "Por favor, preencha todos os campos obrigatórios", + "invalidCron": "Expressão cron inválida", + "connectionTestFailed": "Teste de conexão falhou", + "connectionTestSuccess": "Teste de conexão bem-sucedido!" }, "messages": { - "requiredFields": "Por favor, preencha todos os campos obrigatorios", - "connectionSuccess": "Teste de conexao bem-sucedido!", - "connectionFailed": "Teste de conexao falhou" + "requiredFields": "Por favor, preencha todos os campos obrigatórios", + "connectionSuccess": "Teste de conexão bem-sucedido!", + "connectionFailed": "Teste de conexão falhou" }, - "testingConnection": "Testando conexao...", - "saveSettings": "Salvar Configuracao", + "testingConnection": "Testando conexão...", + "saveSettings": "Salvar Configuração", "savingSettings": "Salvando..." }, "history": { - "searchPlaceholder": "Buscar backups...", + "searchPlaceholder": "Pesquisar backups...", "allStatus": "Todos os Status", "status": { - "completed": "Concluido", + "completed": "Concluído", "failed": "Falhou", "running": "Executando", "partial": "Parcial" @@ -1949,75 +1956,75 @@ "type": "Tipo", "status": "Status", "size": "Tamanho", - "duration": "Duracao", - "actions": "Acoes" + "duration": "Duração", + "actions": "Ações" }, "columns": { "status": "Status", "dateTime": "Data e Hora", "type": "Tipo", "size": "Tamanho", - "duration": "Duracao", - "actions": "Acoes" + "duration": "Duração", + "actions": "Ações" }, "details": { "backupDetails": "Detalhes do Backup", "destination": "Destino", "started": "Iniciado", - "completed": "Concluido", - "contentBackedUp": "Conteudo Salvo", + "completed": "Concluído", + "contentBackedUp": "Conteúdo no Backup", "errorDetails": "Detalhes do Erro", "manifest": "Manifesto" }, - "statistics": "Estatisticas", + "statistics": "Estatísticas", "errors": "Erros", "backupDetails": { "backupId": "ID do Backup", - "startTime": "Hora de Inicio", - "endTime": "Hora de Termino", + "startTime": "Hora de Início", + "endTime": "Hora de Término", "destination": "Destino", "filesProcessed": "Arquivos Processados", "totalSize": "Tamanho Total", - "compressionRatio": "Taxa de Compressao", + "compressionRatio": "Taxa de Compressão", "errorLog": "Log de Erros", "noErrors": "Nenhum erro ocorreu" }, "pagination": { - "showing": "Mostrando {{from}}-{{to}} de {{total}} backups", + "showing": "Exibindo {{from}}-{{to}} de {{total}} backups", "previous": "Anterior", - "next": "Proximo" + "next": "Próximo" }, "filter": { "allStatus": "Todos os Status", - "completed": "Concluido", + "completed": "Concluído", "failed": "Falhou", "running": "Executando", "partial": "Parcial" }, "noBackupsFound": "Nenhum backup encontrado", - "backupsWillAppear": "Backups aparecerao aqui depois de criados", + "backupsWillAppear": "Os backups aparecerão aqui quando criados", "messages": { - "deleteSuccess": "Backup excluido com sucesso" + "deleteSuccess": "Backup excluído com sucesso" } }, "restore": { "steps": { "selectSource": "Selecionar Origem", "chooseBackup": "Escolher Backup", - "restoreOptions": "Opcoes de Restauracao", + "restoreOptions": "Opções de Restauração", "reviewConfirm": "Revisar e Confirmar", - "progress": "Progresso da Restauracao" + "progress": "Progresso da Restauração" }, "source": { "title": "Selecionar Origem do Backup", "subtitle": "Escolha de onde restaurar o backup", "local": { "name": "Backup Local", - "description": "Restaurar do sistema de arquivos local" + "description": "Restaurar a partir do sistema de arquivos local" }, "s3": { "name": "Armazenamento S3", - "description": "Restaurar do bucket S3" + "description": "Restaurar a partir do bucket S3" }, "upload": { "name": "Enviar Backup", @@ -2025,7 +2032,7 @@ "comingSoon": "Funcionalidade de upload em breve" }, "configuration": { - "s3": "Configuracao S3", + "s3": "Configuração S3", "endpoint": "URL do Endpoint S3", "bucket": "Nome do Bucket", "accessKey": "ID da Chave de Acesso", @@ -2034,143 +2041,143 @@ }, "backup": { "title": "Escolher Backup para Restaurar", - "subtitle": "Selecione entre os backups disponiveis", + "subtitle": "Selecione entre os backups disponíveis", "noBackupsFound": "Nenhum backup encontrado na origem selecionada", "encrypted": "Backup Criptografado", - "encryptedMessage": "Voce precisara fornecer a frase de criptografia para restaurar este backup.", - "enterPassphrase": "Digite a frase de criptografia", - "at": "as" + "encryptedMessage": "Você precisará fornecer a senha de criptografia para restaurar este backup.", + "enterPassphrase": "Digite a senha de criptografia", + "at": "às" }, "restoreTypes": { "full": { - "name": "Restauracao Completa", - "description": "Restaurar tudo incluindo banco de dados, fotos e arquivos", - "warning": "Isto substituira todos os dados atuais" + "name": "Restauração Completa", + "description": "Restaurar tudo, incluindo banco de dados, fotos e arquivos", + "warning": "Isso substituirá todos os dados atuais" }, "database": { - "name": "Apenas Banco de Dados", - "description": "Restaurar apenas o banco de dados (configuracoes, eventos, usuarios)", - "warning": "O banco de dados atual sera substituido" + "name": "Somente Banco de Dados", + "description": "Restaurar apenas o banco de dados (configurações, eventos, usuários)", + "warning": "O banco de dados atual será substituído" }, "files": { - "name": "Apenas Arquivos", + "name": "Somente Arquivos", "description": "Restaurar apenas fotos e arquivos", "warning": "Arquivos existentes podem ser sobrescritos" }, "selective": { - "name": "Restauracao Seletiva", - "description": "Escolha itens especificos para restaurar", - "warning": "Apenas itens selecionados serao restaurados" + "name": "Restauração Seletiva", + "description": "Escolher itens específicos para restaurar", + "warning": "Apenas os itens selecionados serão restaurados" } }, "options": { - "title": "Opcoes de Restauracao", + "title": "Opções de Restauração", "subtitle": "Escolha o que restaurar", "additionalOptions": { - "title": "Opcoes Adicionais", - "skipPreBackup": "Pular Backup Pre-Restauracao", - "skipPreBackupHelp": "Por padrao, um backup e criado antes da restauracao. Marque para pular.", - "force": "Forcar Restauracao", - "forceHelp": "Ignorar verificacoes de seguranca e avisos (use com cuidado)" + "title": "Opções Adicionais", + "skipPreBackup": "Ignorar Backup Pré-Restauração", + "skipPreBackupHelp": "Por padrão, um backup é criado antes da restauração. Marque esta opção para ignorar.", + "force": "Forçar Restauração", + "forceHelp": "Ignorar verificações e avisos de segurança (use com cuidado)" } }, "confirmation": { "title": "Revisar e Confirmar", - "subtitle": "Por favor, revise sua configuracao de restauracao", + "subtitle": "Por favor, revise sua configuração de restauração", "validation": { - "passed": "Validacao Aprovada", - "failed": "Validacao Falhou", - "checking": "Validando configuracao de restauracao..." + "passed": "Validação Aprovada", + "failed": "Validação Falhou", + "checking": "Validando configuração de restauração..." }, "spaceCheck": { - "title": "Espaco de Armazenamento", - "required": "Necessario", - "available": "Disponivel", - "insufficient": "Espaco de armazenamento insuficiente" + "title": "Espaço de Armazenamento", + "required": "Necessário", + "available": "Disponível", + "insufficient": "Espaço de armazenamento insuficiente" }, "summary": { - "title": "Resumo da Restauracao", + "title": "Resumo da Restauração", "source": "Origem", "backupDate": "Data do Backup", - "restoreType": "Tipo de Restauracao", - "preBackup": "Pre-backup", + "restoreType": "Tipo de Restauração", + "preBackup": "Pré-backup", "enabled": "Ativado", "skipped": "Ignorado" }, "warning": { "title": "Aviso Importante", - "message": "Esta operacao de restauracao substituira dados existentes. Certifique-se de ter um backup atual antes de prosseguir. Esta acao nao pode ser desfeita." + "message": "Esta operação de restauração substituirá os dados existentes. Certifique-se de ter um backup atual antes de continuar. Esta ação não pode ser desfeita." } }, "progress": { - "title": "Progresso da Restauracao", - "inProgress": "Restauracao em andamento...", - "completed": "Restauracao concluida", + "title": "Progresso da Restauração", + "inProgress": "Restauração em andamento...", + "completed": "Restauração concluída", "overallProgress": "Progresso Geral", "current": "Atual", "statusDetails": "Detalhes do Status", - "restoreLogs": "Logs de Restauracao", + "restoreLogs": "Logs de Restauração", "steps": { - "completed": "Concluido", + "completed": "Concluído", "running": "Executando", "failed": "Falhou", "pending": "Pendente" }, "success": { - "title": "Restauracao Concluida com Sucesso", - "message": "Seus dados foram restaurados. Por favor, verifique se tudo esta funcionando corretamente." + "title": "Restauração Concluída com Sucesso", + "message": "Seus dados foram restaurados. Por favor, verifique se tudo está funcionando corretamente." } }, "actions": { "back": "Voltar", - "next": "Proximo", - "startRestore": "Iniciar Restauracao", + "next": "Próximo", + "startRestore": "Iniciar Restauração", "starting": "Iniciando...", "validating": "Validando...", - "startNewRestore": "Iniciar Nova Restauracao" + "startNewRestore": "Iniciar Nova Restauração" }, "messages": { - "restoreStarted": "Restauracao iniciada com sucesso" + "restoreStarted": "Restauração iniciada com sucesso" } }, "messages": { "backupStarted": "Backup iniciado com sucesso", "backupFailed": "Falha ao iniciar backup", - "configUpdated": "Configuracao de backup atualizada", - "configUpdateFailed": "Falha ao atualizar configuracao", - "backupDeleted": "Backup excluido com sucesso", + "configUpdated": "Configuração de backup atualizada", + "configUpdateFailed": "Falha ao atualizar configuração", + "backupDeleted": "Backup excluído com sucesso", "deleteFailed": "Falha ao excluir backup", - "testEmailSent": "Teste de conexao bem-sucedido!", - "testEmailFailed": "Teste de conexao falhou" + "testEmailSent": "Teste de conexão bem-sucedido!", + "testEmailFailed": "Teste de conexão falhou" } }, "cssTemplates": { - "title": "Templates CSS Personalizados", - "template": "Template", - "templateName": "Nome do Template", - "enableTemplate": "Ativar este template", - "enableHint": "Templates ativados podem ser selecionados ao criar eventos", - "cssContent": "Conteudo CSS", - "cssHint": "Use .gallery-page para limitar estilos a galeria. Variaveis disponiveis: --gallery-bg, --gallery-text, --gallery-accent", - "securityNotice": "Aviso de Seguranca", - "securityText": "O CSS e sanitizado para prevenir codigo malicioso. URLs externas, @import e expressoes JavaScript sao bloqueadas.", - "resetToDefault": "Restaurar Padrao", - "resetConfirm": "Restaurar este template para o padrao? Suas alteracoes serao perdidas.", - "unsavedChanges": "Alteracoes nao salvas", - "saveTemplate": "Salvar Template", - "lastUpdated": "Ultima atualizacao", - "saved": "Template salvo com sucesso", - "saveFailed": "Falha ao salvar template", - "sanitizationWarning": "Alguns padroes CSS foram bloqueados por seguranca", - "reset": "Template restaurado para o padrao", - "resetFailed": "Falha ao restaurar template" + "title": "Modelos de CSS Personalizado", + "template": "Modelo", + "templateName": "Nome do Modelo", + "enableTemplate": "Ativar este modelo", + "enableHint": "Modelos ativados podem ser selecionados ao criar eventos", + "cssContent": "Conteúdo CSS", + "cssHint": "Use .gallery-page para aplicar estilos à galeria. Variáveis disponíveis: --gallery-bg, --gallery-text, --gallery-accent", + "securityNotice": "Aviso de Segurança", + "securityText": "O CSS é sanitizado para evitar código malicioso. URLs externas, @import e expressões JavaScript são bloqueados.", + "resetToDefault": "Redefinir para Padrão", + "resetConfirm": "Redefinir este modelo para o padrão? Suas alterações serão perdidas.", + "unsavedChanges": "Alterações não salvas", + "saveTemplate": "Salvar Modelo", + "lastUpdated": "Última atualização", + "saved": "Modelo salvo com sucesso", + "saveFailed": "Falha ao salvar modelo", + "sanitizationWarning": "Alguns padrões CSS foram bloqueados por segurança", + "reset": "Modelo redefinido para o padrão", + "resetFailed": "Falha ao redefinir modelo" }, "maintenance": { - "title": "Manutencao do Sistema", - "message": "Estamos realizando manutencao programada para melhorar nosso servico. Voltaremos em breve.", - "expectedCompletion": "Previsao de conclusao:", + "title": "Manutenção do Sistema", + "message": "Estamos realizando manutenção programada para melhorar nosso serviço. Voltaremos em breve.", + "expectedCompletion": "Previsão de conclusão:", "checkBackLater": "Por favor, volte mais tarde", - "urgentMatters": "Para assuntos urgentes, entre em contato" + "urgentMatters": "Para assuntos urgentes, entre em contato com" }, "passwordChange": { "title": "Alterar Senha", @@ -2180,96 +2187,96 @@ "currentPasswordPlaceholder": "Digite a senha atual", "newPasswordPlaceholder": "Digite a nova senha", "confirmPasswordPlaceholder": "Confirme a nova senha", - "requirements": "Requisitos da Senha:", + "requirements": "Requisitos de Senha:", "minLength": "Pelo menos 6 caracteres", "mustDiffer": "Deve ser diferente da senha atual", "success": "Senha alterada com sucesso", "failed": "Falha ao alterar senha", - "currentRequired": "Senha atual e obrigatoria", - "newRequired": "Nova senha e obrigatoria", - "minLengthError": "Senha deve ter pelo menos 6 caracteres", + "currentRequired": "A senha atual é obrigatória", + "newRequired": "A nova senha é obrigatória", + "minLengthError": "A senha deve ter pelo menos 6 caracteres", "confirmRequired": "Por favor, confirme sua nova senha", - "noMatch": "Senhas nao conferem", - "mustBeDifferent": "Nova senha deve ser diferente da senha atual", + "noMatch": "As senhas não coincidem", + "mustBeDifferent": "A nova senha deve ser diferente da senha atual", "cancel": "Cancelar" }, "mandatoryPasswordChange": { - "title": "Alteracao de Senha Obrigatoria", - "description": "Por razoes de seguranca, voce deve alterar sua senha antes de acessar o painel admin.", - "success": "Senha alterada com sucesso! Voce ja pode acessar o painel admin.", + "title": "Alteração de Senha Obrigatória", + "description": "Por razões de segurança, você deve alterar sua senha antes de acessar o painel de administração.", + "success": "Senha alterada com sucesso! Você já pode acessar o painel de administração.", "minLength": "Pelo menos 12 caracteres", - "mustContainUpperLower": "Deve conter letras maiusculas e minusculas", - "mustContainNumbers": "Deve conter numeros", + "mustContainUpperLower": "Deve conter letras maiúsculas e minúsculas", + "mustContainNumbers": "Deve conter números", "mustContainSpecial": "Deve conter caracteres especiais (!@#$%^&*)", - "minLengthError": "Senha deve ter pelo menos 12 caracteres", - "mustContainLowercase": "Senha deve conter letras minusculas", - "mustContainUppercase": "Senha deve conter letras maiusculas", - "mustContainNumbersError": "Senha deve conter numeros", - "mustContainSpecialError": "Senha deve conter caracteres especiais" + "minLengthError": "A senha deve ter pelo menos 12 caracteres", + "mustContainLowercase": "A senha deve conter letras minúsculas", + "mustContainUppercase": "A senha deve conter letras maiúsculas", + "mustContainNumbersError": "A senha deve conter números", + "mustContainSpecialError": "A senha deve conter caracteres especiais" }, "offline": { - "backOnline": "De volta online", - "noConnection": "Sem conexao com a internet" + "backOnline": "Conexão restabelecida", + "noConnection": "Sem conexão com a internet" }, "download": { "downloading": "Baixando...", - "percentComplete": "% concluido" + "percentComplete": "% concluído" }, "passwordGenerator": { "weak": "Fraca", - "fair": "Razoavel", + "fair": "Razoável", "good": "Boa", "strong": "Forte", "generating": "Gerando...", "generatePassword": "Gerar Senha", - "showSuggestions": "Mostrar sugestoes de senha", - "moreOptions": "Mais Opcoes", - "suggestions": "Sugestoes de Senha", + "showSuggestions": "Mostrar sugestões de senha", + "moreOptions": "Mais Opções", + "suggestions": "Sugestões de Senha", "characters": "caracteres", "copyPassword": "Copiar senha", "use": "Usar", - "pattern": "Padrao:", - "patternDescription": "As senhas sao geradas usando o nome e a data do seu evento. Exemplo: \"Local2024$Agosto\" para melhor seguranca e memorabilidade." + "pattern": "Padrão:", + "patternDescription": "As senhas são geradas usando o nome e a data do seu evento. Exemplo: \"Local2024$Agosto\" para maior segurança e facilidade de memorização." }, "feedback": { - "comments": "Comentarios", - "addComment": "Adicionar Comentario", + "comments": "Comentários", + "addComment": "Adicionar Comentário", "yourName": "Seu nome", "yourEmail": "Seu email", - "writeComment": "Escreva um comentario...", + "writeComment": "Escreva um comentário...", "submit": "Enviar", - "commentSubmitted": "Comentario enviado", - "commentError": "Falha ao enviar comentario", + "commentSubmitted": "Comentário enviado", + "commentError": "Falha ao enviar comentário", "rateLimited": "Por favor, aguarde antes de comentar novamente", - "commentRequired": "Comentario e obrigatorio", - "nameRequired": "Nome e obrigatorio", - "emailRequired": "Email e obrigatorio", - "anonymous": "Anonimo", - "pendingApproval": "Aguardando aprovacao", - "noComments": "Nenhum comentario ainda. Seja o primeiro a comentar!", - "rating": "Avaliacao", - "ratePhoto": "Avalie esta foto", - "yourRating": "Sua avaliacao", - "averageRating": "Avaliacao media", - "totalRatings": "avaliacoes", + "commentRequired": "O comentário é obrigatório", + "nameRequired": "O nome é obrigatório", + "emailRequired": "O email é obrigatório", + "anonymous": "Anônimo", + "pendingApproval": "Aguardando aprovação", + "noComments": "Nenhum comentário ainda. Seja o primeiro a comentar!", + "rating": "Avaliação", + "ratePhoto": "Avaliar esta foto", + "yourRating": "Sua avaliação", + "averageRating": "Avaliação média", + "totalRatings": "avaliações", "likes": "Curtidas", "favorites": "Favoritos", "likePhoto": "Curtir esta foto", "favoritePhoto": "Adicionar aos favoritos", "photoFeedback": "Feedback da Foto", "hasFeedback": "Tem feedback", - "hasComments": "Tem comentarios", - "hasRating": "Tem avaliacao", + "hasComments": "Tem comentários", + "hasRating": "Tem avaliação", "settings": { - "title": "Configuracoes de Feedback dos Convidados", + "title": "Configurações de Feedback dos Convidados", "enableFeedback": "Ativar feedback", "feedbackTypes": "Tipos de Feedback", - "ratings": "Avaliacoes com Estrelas", + "ratings": "Avaliações por Estrelas", "ratingsDesc": "Permitir que convidados avaliem fotos (1-5 estrelas)", "likes": "Curtidas", "likesDesc": "Funcionalidade simples de curtir/descurtir", - "comments": "Comentarios", - "commentsDesc": "Comentarios de texto nas fotos", + "comments": "Comentários", + "commentsDesc": "Comentários de texto nas fotos", "favorites": "Favoritos", "favoritesDesc": "Marcar fotos como favoritas" } @@ -2277,40 +2284,65 @@ "filter": { "feedbackFilters": "Filtros de Feedback", "clear": "Limpar", - "rating": "Avaliacao", + "rating": "Avaliação", "allPhotos": "Todas as Fotos", - "anyRating": "Qualquer Avaliacao", - "oneStarPlus": "1+ Estrelas", + "anyRating": "Qualquer Avaliação", + "oneStarPlus": "1+ Estrela", "twoStarsPlus": "2+ Estrelas", "threeStarsPlus": "3+ Estrelas", "fourStarsPlus": "4+ Estrelas", - "fiveStarsOnly": "5 Estrelas Apenas", + "fiveStarsOnly": "Apenas 5 Estrelas", "hasLikes": "Tem curtidas", "hasFavorites": "Tem favoritos", - "hasComments": "Tem comentarios" + "hasComments": "Tem comentários" }, "adminLogin": { - "title": "Login Admin", + "title": "Login de Administrador", "subtitle": "Entre para gerenciar suas galerias de fotos", - "sessionExpired": "Sua sessao expirou. Por favor, faca login novamente.", - "emailLabel": "Endereco de Email", + "sessionExpired": "Sua sessão expirou. Por favor, faça login novamente.", + "emailLabel": "Endereço de email", "emailPlaceholder": "admin@exemplo.com", - "emailRequired": "Email e obrigatorio", - "invalidEmail": "Formato de email invalido", + "emailRequired": "O email é obrigatório", + "invalidEmail": "Formato de email inválido", "passwordLabel": "Senha", "passwordPlaceholder": "Digite sua senha", - "passwordRequired": "Senha e obrigatoria", - "passwordMinLength": "Senha deve ter pelo menos 6 caracteres", + "passwordRequired": "A senha é obrigatória", + "passwordMinLength": "A senha deve ter pelo menos 6 caracteres", "rememberMe": "Lembrar de mim", "forgotPassword": "Esqueceu a senha?", "signIn": "Entrar", "loginSuccess": "Login realizado com sucesso!", - "networkError": "Erro de rede. Por favor, verifique sua conexao e tente novamente.", + "networkError": "Erro de rede. Por favor, verifique sua conexão e tente novamente.", "tooManyAttempts": "Muitas tentativas de login. Por favor, tente novamente mais tarde.", - "invalidCredentials": "Email ou senha invalidos", + "invalidCredentials": "email ou senha inválidos", "generalError": "Ocorreu um erro. Por favor, tente novamente.", - "needHelp": "Precisa de ajuda? Entre em contato", - "poweredBy": "Powered by PicPeak", - "devModeHint": "Modo de Desenvolvimento: Use email: admin@example.com, senha: admin123" + "needHelp": "Precisa de ajuda? Entre em contato com", + "poweredBy": "Desenvolvido por PicPeak", + "devModeHint": "Modo de Desenvolvimento: Use email: admin@exemplo.com, senha: admin123" + }, + "clientAccess": { + "title": "Acesso do Cliente", + "description": "Revise as fotos e gerencie a visibilidade antes de compartilhar a galeria com os convidados.", + "pinLabel": "PIN do Cliente", + "pinPlaceholder": "Digite seu PIN de cliente", + "enterPin": "Por favor, digite seu PIN de cliente", + "invalidPin": "PIN inválido. Por favor, tente novamente.", + "loginFailed": "Autenticação falhou. Por favor, tente novamente.", + "loginButton": "Acessar Galeria", + "guestHint": "Procurando pela galeria de convidados?", + "guestLink": "Ir para a visualização de convidados", + "banner": "Modo Cliente", + "visibleCount": "{{visible}} de {{total}} fotos visíveis para os convidados", + "hideSelected": "Ocultar Selecionadas", + "showSelected": "Mostrar Selecionadas", + "adminTitle": "Acesso do Cliente", + "enableToggle": "Ativar Acesso do Cliente", + "enableDescription": "Permitir que clientes revisem e ocultem fotos antes de a galeria ser compartilhada com os convidados.", + "pinHelperText": "O cliente usará este PIN para acessar a página de revisão.", + "pinUpdated": "PIN do cliente atualizado", + "setPin": "Definir PIN", + "linkLabel": "Link de Acesso do Cliente", + "regenerateToken": "Regenerar link", + "tokenRegenerated": "Link de acesso do cliente regenerado" } } diff --git a/frontend/src/i18n/locales/ru.json b/frontend/src/i18n/locales/ru.json index e1eaf341..6218704b 100644 --- a/frontend/src/i18n/locales/ru.json +++ b/frontend/src/i18n/locales/ru.json @@ -1449,6 +1449,13 @@ "username": "Выберите имя пользователя", "password": "Создайте пароль", "submit": "Создать аккаунт" + }, + "photos": { + "hidden": "Скрыто", + "hideSelected": "Скрыть", + "showSelected": "Показать", + "hiddenSuccess": "Фотографии скрыты от гостей", + "visibleSuccess": "Фотографии теперь видны гостям" } }, "permissions": { @@ -2312,5 +2319,30 @@ "needHelp": "Нужна помощь? Свяжитесь", "poweredBy": "Работает на PicPeak", "devModeHint": "Режим разработки: используйте email: admin@example.com, пароль: admin123" + }, + "clientAccess": { + "title": "Доступ клиента", + "description": "Введите PIN-код для просмотра и выбора фотографий перед публикацией.", + "pinLabel": "PIN-код клиента", + "pinPlaceholder": "Введите PIN-код", + "enterPin": "Введите PIN-код, который вы получили", + "invalidPin": "Неверный PIN-код. Попробуйте ещё раз.", + "loginFailed": "Не удалось войти. Попробуйте ещё раз.", + "loginButton": "Получить доступ", + "guestHint": "Просто хотите посмотреть фотографии?", + "guestLink": "Перейти в гостевую галерею", + "banner": "Режим клиента", + "visibleCount": "{{visible}} из {{total}} фотографий видны гостям", + "hideSelected": "Скрыть выбранные", + "showSelected": "Показать выбранные", + "adminTitle": "Доступ клиента", + "enableToggle": "Включить доступ клиента", + "enableDescription": "Позволить клиентам просматривать и скрывать фотографии до того, как галерея будет открыта для гостей.", + "pinHelperText": "Клиент будет использовать этот PIN-код для доступа к странице проверки.", + "pinUpdated": "PIN-код клиента обновлён", + "setPin": "Установить PIN", + "linkLabel": "Ссылка для клиента", + "regenerateToken": "Сгенерировать новую ссылку", + "tokenRegenerated": "Ссылка для клиента обновлена" } } \ No newline at end of file diff --git a/frontend/src/pages/ClientAccessPage.tsx b/frontend/src/pages/ClientAccessPage.tsx new file mode 100644 index 00000000..180c312b --- /dev/null +++ b/frontend/src/pages/ClientAccessPage.tsx @@ -0,0 +1,207 @@ +import React, { useState } from 'react'; +import { useParams, useSearchParams, useNavigate, Link } from 'react-router-dom'; +import { AlertCircle, Lock } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; + +import { Card, CardContent, Input, Button, Loading } from '../components/common'; +import { useGalleryAuth } from '../contexts'; +import { useGalleryInfo } from '../hooks/useGallery'; +import { api } from '../config/api'; +import { buildResourceUrl } from '../utils/url'; + +export const ClientAccessPage: React.FC = () => { + const { slug } = useParams<{ slug: string }>(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const { isAuthenticated, isClient, clientLogin, isLoading: authLoading } = useGalleryAuth(); + const { t } = useTranslation(); + const [pin, setPin] = useState(''); + const [isLoggingIn, setIsLoggingIn] = useState(false); + const [loginError, setLoginError] = useState(null); + + const { data: galleryInfo, isLoading: isLoadingInfo, error: infoError } = useGalleryInfo(slug); + + const { data: settingsData } = useQuery({ + queryKey: ['gallery-settings'], + queryFn: async () => { + const response = await api.get('/public/settings'); + return response.data; + }, + staleTime: 5 * 60 * 1000, + }); + + // If already authenticated as client, redirect to gallery + React.useEffect(() => { + if (isAuthenticated && isClient && slug) { + navigate(`/gallery/${slug}`, { replace: true }); + } + }, [isAuthenticated, isClient, slug, navigate]); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!pin.trim()) { + setLoginError(t('clientAccess.enterPin')); + return; + } + + if (!slug) { + setLoginError(t('errors.galleryNotFound')); + return; + } + + try { + setIsLoggingIn(true); + setLoginError(null); + await clientLogin(slug, pin); + navigate(`/gallery/${slug}`, { replace: true }); + } catch (error: any) { + const statusCode = error.response?.status; + if (statusCode === 401) { + setLoginError(t('clientAccess.invalidPin')); + } else if (statusCode === 423) { + setLoginError(t('auth.tooManyAttempts')); + } else { + setLoginError(t('clientAccess.loginFailed')); + } + } finally { + setIsLoggingIn(false); + } + }; + + if (isLoadingInfo || authLoading) { + return ( +
+
+ +
+
+ ); + } + + if (infoError || !galleryInfo) { + return ( +
+
+ {settingsData?.branding_logo_url && ( +
+ {settingsData.branding_company_name +
+ )} +
+ + + +

{t('errors.galleryNotFound')}

+

{t('errors.galleryNotFoundMessage')}

+
+
+
+
+
+ ); + } + + return ( +
+
+ {/* Logo */} + {settingsData?.branding_logo_url && ( +
+ {settingsData.branding_company_name +
+ )} + +
+ + +
+
+ +
+

+ {t('clientAccess.title')} +

+

+ {galleryInfo.event_name} +

+

+ {t('clientAccess.description')} +

+
+ +
+ { + setPin(e.target.value); + setLoginError(null); + }} + error={loginError || undefined} + leftIcon={} + autoFocus + /> + + + + +
+

+ {t('clientAccess.guestHint')}{' '} + + {t('clientAccess.guestLink')} + +

+
+
+
+
+ + {/* Footer */} +
+
+ + {t('legal.impressum')} + + | + + {t('legal.datenschutz')} + +
+

+ Powered by PicPeak +

+
+
+
+ ); +}; diff --git a/frontend/src/pages/admin/CreateEventPage.tsx b/frontend/src/pages/admin/CreateEventPage.tsx index 5d8b6052..fe8c2fb5 100644 --- a/frontend/src/pages/admin/CreateEventPage.tsx +++ b/frontend/src/pages/admin/CreateEventPage.tsx @@ -9,7 +9,8 @@ import { Palette, Eye, EyeOff, - Image + Image, + Key } from 'lucide-react'; import { addDays } from 'date-fns'; import { toast } from 'react-toastify'; @@ -59,6 +60,9 @@ interface FormData { rate_limit_window_minutes?: number; rate_limit_max_requests?: number; }; + // Client access (#172) + client_access_enabled: boolean; + client_password: string; } // Fallback event types (used when API is unavailable) @@ -114,8 +118,10 @@ export const CreateEventPage: React.FC = () => { rate_limit_window_minutes: 15, rate_limit_max_requests: 10, }, + client_access_enabled: false, + client_password: '', }); - + const [errors, setErrors] = useState>>({}); const [showPassword, setShowPassword] = useState(false); @@ -310,6 +316,9 @@ export const CreateEventPage: React.FC = () => { require_name_email: feedbackSettings.require_name_email, moderate_comments: feedbackSettings.moderate_comments, show_feedback_to_guests: feedbackSettings.show_feedback_to_guests, + // Client access (#172) + client_access_enabled: formData.client_access_enabled, + client_password: formData.client_access_enabled ? formData.client_password : undefined, }; createMutation.mutate(payload); @@ -742,6 +751,44 @@ export const CreateEventPage: React.FC = () => {
+ {/* Client Access (#172) */} +
+ + + {formData.client_access_enabled && ( +
+ } + helperText={t('clientAccess.pinHelperText')} + /> +
+ )} +
+ {/* User Upload Settings */}