screentinker/frontend/js/i18n/en.js
ScreenTinker 303c83e86a feat(ai): generate background + foreground images for signs (#41 Phase 2)
A prompt now produces a full sign: the LLM writes the design AND image prompts,
the server generates the images and composites them with the crisp text layer.

- lib/image-gen.js: text-to-image with 3 BYO/self-hostable backends, all behind
  the SSRF guard: 'sdcpp' (local stable-diffusion.cpp OpenAI-compatible server,
  exact small sizes that fit VRAM), 'openai' (cloud / OpenAI-compatible, snapped
  sizes), 'comfyui' (prompt/history/view API).
- ai.js: prompt asks for a background_prompt (preferred — full-bleed atmosphere)
  and an optional foreground image element; after the design is normalized, the
  bg + fg images are generated best-effort (a failed image never fails the sign)
  and returned as data URLs. New image_* settings (provider/base_url/model),
  image_provider whitelist, schema column + migration.
- designer.js: AI-images section in settings; generate applies the background
  image; publish bakes the background image into the HTML so it survives.
- server.js: raise JSON body limit to 12mb for embedded image data URLs.

Verified end-to-end on local Vulkan SDXL (RTX 5090): prompt -> bg+fg images on
the canvas -> publish creates a widget with the images embedded. 63/63.

Note: prod (not self-hosted) requires a PUBLIC image endpoint (e.g. OpenAI); the
SSRF guard blocks localhost there. Follow-up: upload generated images to the
content store and reference by URL to avoid multi-MB widget configs.
2026-06-09 13:40:14 -05:00

1347 lines
65 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// English translations. This file is the source of truth for keys —
// every other locale should mirror its keys (or fall back to en).
export default {
// Nav (sidebar)
'nav.displays': 'Displays',
'nav.content': 'Content',
'nav.playlists': 'Playlists',
'nav.layouts': 'Layouts',
'nav.widgets': 'Widgets',
'nav.schedule': 'Schedule',
'nav.walls': 'Video Walls',
'nav.reports': 'Reports',
'nav.kiosk': 'Kiosk',
'nav.designer': 'Designer',
'nav.activity': 'Activity',
'nav.teams': 'Teams',
'nav.help': 'Help',
'nav.settings': 'Settings',
'nav.subscription': 'Subscription',
'nav.admin': 'Admin',
// Common (shared across views)
'common.save': 'Save',
'common.cancel': 'Cancel',
'common.delete': 'Delete',
'common.edit': 'Edit',
'common.done': 'Done',
'common.saving': 'Saving...',
'common.deleting': 'Deleting...',
'common.loading': 'Loading...',
'confirm_delete.type_label': 'Type "{name}" to confirm',
'confirm_delete.failed': 'Action failed',
'common.connected': 'Connected',
'common.disconnected': 'Disconnected',
'common.never': 'Never',
'common.just_now': 'Just now',
'common.minutes_ago': '{n}m ago',
'common.hours_ago': '{n}h ago',
'common.days_ago': '{n}d ago',
'common.unknown': 'Unknown',
// Auth (login view)
'auth.sign_in': 'Sign In',
'auth.sign_out': 'Sign out',
'auth.create_account': 'Create Account',
'auth.create_admin_account': 'Create Admin Account',
'auth.email': 'Email',
'auth.password': 'Password',
'auth.name': 'Name',
'auth.placeholder_email': 'you@example.com',
'auth.placeholder_password': '••••••••',
'auth.placeholder_name': 'Your name',
'auth.placeholder_register_password': 'At least 6 characters',
'auth.subtitle_setup': 'Create your admin account to get started',
'auth.subtitle_signin': 'Sign in to manage your displays',
'auth.trial_notice': 'New accounts get a 14-day free Pro trial',
'auth.divider_or': 'OR',
'auth.signin_google': 'Sign in with Google',
'auth.signin_microsoft': 'Sign in with Microsoft',
'auth.back_to_signin': 'Back to Sign In',
'auth.support_access': 'Support Access',
'auth.support_token_placeholder': 'Paste support token',
'auth.support_authenticate': 'Authenticate with Support Token',
'auth.terms': 'Terms of Service',
'auth.privacy': 'Privacy Policy',
'auth.error_email_password_required': 'Email and password required',
'auth.error_password_min_6': 'Password must be at least 6 characters',
'auth.error_login_failed': 'Login failed',
'auth.error_registration_failed': 'Registration failed',
'auth.error_paste_support_token': 'Paste a support token',
'auth.error_support_failed': 'Support login failed',
'auth.error_google_failed': 'Google sign-in failed',
'auth.error_microsoft_failed': 'Microsoft sign-in failed',
// Dashboard
'dashboard.title': 'Displays',
'dashboard.subtitle': 'Manage your remote displays',
'dashboard.help_tip': 'Your paired display devices. Green = online, red = offline. Click a device to manage its playlist, view telemetry, or use remote control.',
'dashboard.add': 'Add Display',
'dashboard.create_group': '+ Group',
'dashboard.search': 'Search displays...',
'dashboard.all_status': 'All Status',
'dashboard.online': 'Online',
'dashboard.offline': 'Offline',
'dashboard.awaiting_pairing': 'Awaiting Pairing',
'dashboard.no_preview': 'No preview available',
'dashboard.total_displays': 'Total Displays',
'dashboard.ungrouped': 'Ungrouped',
'dashboard.no_displays': 'No displays yet',
'dashboard.no_displays_desc': 'Install the ScreenTinker app on your TV and pair it using the button above.',
'dashboard.failed_to_load': 'Failed to load displays',
'dashboard.unknown_playlist': 'Unknown playlist',
'dashboard.mixed_playlists': 'Mixed playlists',
'dashboard.playlist_label': 'Playlist: {name}',
'dashboard.devices_count_one': '{n} device',
'dashboard.devices_count_other': '{n} devices',
'dashboard.online_count': '{n} online',
'dashboard.set_playlist_placeholder': 'Set Playlist...',
'dashboard.send_command_placeholder': 'Send Command...',
'dashboard.manage': 'Manage',
'dashboard.manage_tooltip': 'Add/remove devices',
'dashboard.delete_group_tooltip': 'Delete group',
'dashboard.no_devices_in_group': 'No devices in this group. Click Manage to add some.',
'dashboard.manage_group_subtitle': 'Check devices to add them to this group',
'dashboard.draft_suffix': '(draft)',
// Group commands
'dashboard.cmd.screen_on': 'Screen On',
'dashboard.cmd.screen_off': 'Screen Off',
'dashboard.cmd.restart_app': 'Restart App',
'dashboard.cmd.check_update': 'Check Update',
'dashboard.cmd.reboot': 'Reboot',
'dashboard.cmd.shutdown': 'Shutdown',
// Dashboard prompts/confirms
'dashboard.prompt_group_name': 'Group name:',
'dashboard.error_pairing_code': 'Enter a valid 6-digit pairing code',
'dashboard.confirm_add_to_group': '{name} is already in: {groups}\n\nAdd it to "{target}" too?',
'dashboard.confirm_assign_playlist': 'Assign playlist "{playlist}" to all devices in "{group}"?',
'dashboard.confirm_destructive_command': '{cmd} all {n} devices in "{group}"?\n\nThis cannot be undone.',
'dashboard.confirm_delete_group': 'Delete this group? Devices will not be affected.',
// Dashboard toasts
'dashboard.toast.display_paired': 'Display paired successfully!',
'dashboard.toast.group_created': 'Group created',
'dashboard.toast.group_deleted': 'Group deleted',
'dashboard.toast.already_in_group': '{name} is already in {group}',
'dashboard.toast.moved_device': 'Moved {name} to {group}',
'dashboard.toast.removed_device_one': 'Removed {name} from 1 group',
'dashboard.toast.removed_device_other': 'Removed {name} from {n} groups',
'dashboard.toast.playlist_assigned_one': 'Playlist assigned to 1 device',
'dashboard.toast.playlist_assigned_other': 'Playlist assigned to {n} devices',
'dashboard.toast.command_sent': '{cmd} sent to {sent}/{total} devices',
'dashboard.toast.command_sent_with_offline': '{cmd} sent to {sent}/{total} devices ({offline} offline)',
// Content library
'content.title': 'Content Library',
'content.subtitle': 'Upload and manage your media files',
'content.help_tip': 'Upload videos and images here. Select multiple files for bulk upload. Use Remote URL to stream from external sources. Click a thumbnail to preview.',
'content.drop': 'Drop files here, or click to select one or more',
'content.upload_hint': 'Supports MP4, WebM, AVI, MKV, JPEG, PNG, GIF, WebP',
'content.upload_progress': 'Uploading...',
'content.upload_progress_named': 'Uploading {name}...',
'content.upload_progress_named_pct': 'Uploading {name}... {pct}%',
// Remote URL panel
'content.remote_url': 'Remote URL',
'content.remote_desc': 'Stream directly from a URL. Saves local bandwidth.',
'content.remote_url_placeholder': 'https://example.com/video.mp4',
'content.remote_name_placeholder': 'Display name (optional)',
'content.remote_add_btn': 'Add Remote URL',
// YouTube panel
'content.youtube': 'YouTube',
'content.youtube_desc': 'Embed a YouTube video on your displays.',
'content.youtube_url_placeholder': 'https://youtube.com/watch?v=...',
'content.youtube_name_placeholder': 'Display name (optional)',
'content.youtube_add_btn': 'Add YouTube Video',
// Search / folders
'content.search_placeholder': 'Search content...',
'content.new_folder_btn': '+ New Folder',
'content.breadcrumb_root': 'All Content',
'content.rename_btn': 'Rename',
'content.delete_folder_btn': 'Delete folder',
'content.prompt_folder_name': 'Folder name:',
'content.prompt_rename_folder': 'Rename folder:',
'content.confirm_delete_folder': 'Delete this folder? Content inside moves back to the root level. Subfolders will also be deleted.',
// Empty states
'content.empty_folder_title': 'This folder is empty',
'content.empty_folder_desc': 'Drag content here, or use the Move action.',
'content.no_content': 'No content yet',
'content.no_content_desc': 'Upload videos and images to get started.',
'content.failed_to_load': 'Failed to load content',
// Item type labels
'content.type_youtube': 'YouTube',
'content.type_remote': 'Remote URL',
'content.type_remote_short': 'Remote',
'content.type_video': 'Video',
'content.type_image': 'Image',
// Item action buttons
'content.btn_edit': 'Edit',
'content.btn_delete': 'Delete',
'content.btn_confirm_delete': 'Confirm Delete?',
'content.btn_deleting': 'Deleting...',
// Edit modal
'content.edit_modal_title': 'Edit Content',
'content.label_filename': 'Filename / Display Name',
'content.label_remote_url_field': 'Remote URL',
'content.label_mime_type': 'MIME Type',
'content.label_folder': 'Folder',
'content.label_replace_file': 'Replace File',
'content.replace_file_hint': 'Leave empty to keep current file',
'content.folder_root_option': '— Root —',
'content.save_changes': 'Save Changes',
// MIME options
'content.mime.video_mp4': 'Video (MP4)',
'content.mime.video_webm': 'Video (WebM)',
'content.mime.image_jpeg': 'Image (JPEG)',
'content.mime.image_png': 'Image (PNG)',
'content.mime.image_gif': 'Image (GIF)',
'content.mime.image_webp': 'Image (WebP)',
// Content errors / toasts
'content.error_enter_url': 'Enter a URL',
'content.error_enter_youtube_url': 'Enter a YouTube URL',
'content.error_update_failed': 'Update failed',
'content.toast.remote_added': 'Remote content added',
'content.toast.youtube_added': 'YouTube video added',
'content.toast.deleted': 'Content deleted',
'content.toast.updated': 'Content updated',
'content.toast.uploaded_named': '{name} uploaded successfully',
'content.toast.upload_failed_named': 'Failed to upload {name}: {error}',
'content.toast.folder_created_named': 'Folder "{name}" created',
'content.toast.folder_renamed': 'Folder renamed',
'content.toast.folder_deleted': 'Folder deleted',
'content.toast.moved': 'Moved',
'content.toast.moved_to_root': 'Moved to root',
// Device detail
'device.back': 'Back to Displays',
'device.owner_label': 'Owner: {owner}',
'device.rename': 'Rename',
'device.screenshot_btn': 'Screenshot',
'device.remove': 'Remove',
'device.click_to_confirm': 'Click again to confirm',
'device.prompt_new_name': 'Enter new name:',
'device.confirm_discard_draft': 'Discard all unpublished changes and revert to the last published version?',
'device.failed_load': 'Failed to load device',
'device.no_screenshot': 'No screenshot available. Click "Screenshot" to capture one.',
'device.no_content_assigned': 'No content assigned',
'device.now_playing_id': 'Playing: {id}',
'device.playlist_count_one': '1 item in playlist',
'device.playlist_count_other': '{n} items in playlist',
// Tabs
'device.tab.now_playing': 'Now Playing',
'device.tab.now_playing_tip': "Live screenshot of what's currently displaying on this device.",
'device.tab.playlist': 'Playlist',
'device.tab.playlist_tip': 'Content assigned to this device. Drag items to reorder. Add media, widgets, or kiosk pages.',
'device.tab.info': 'Device Info',
'device.tab.info_tip': 'Hardware telemetry, orientation settings, notes, and device controls.',
'device.tab.remote': 'Remote Control',
'device.tab.remote_tip': 'View the device screen in real-time and send key presses. Works on Android APK and web player.',
// Draft banner
'device.draft.banner_title': 'Unpublished changes',
'device.draft.devices_showing_published': 'Devices are still showing the last published version.',
'device.draft.never_published': 'This playlist has never been published. Devices will show nothing until you publish.',
'device.draft.discard': 'Discard',
'device.draft.publish': 'Publish',
'device.draft.publishing': 'Publishing...',
// Layout selector
'device.layout.label': 'Screen Layout',
'device.layout.fullscreen_default': 'Fullscreen (default)',
'device.layout.zones_count': '{name} ({n} zones)',
'device.layout.template_zones_count': '[Template] {name} ({n} zones)',
'device.layout.apply': 'Apply',
// Playlist tab
'device.playlist.label': 'Playlist',
'device.playlist.no_playlist': 'No playlist',
'device.playlist.copy_to_btn': 'Copy To...',
'device.playlist.add_content_btn': 'Add Content',
'device.playlist.empty_title': 'No content assigned',
'device.playlist.empty_desc': "Add content from your library to this display's playlist.",
'device.playlist_picker.with_count': '{name} — {n} items',
'device.playlist_picker.with_auto': '{name} (auto) — {n} items',
// Info cards
'device.info.status': 'Status',
'device.info.ip_address': 'IP Address',
'device.info.battery': 'Battery',
'device.info.storage': 'Storage',
'device.info.size_free': '{size} free',
'device.info.player_type': 'Player Type',
'device.info.web_player': 'Web Player',
'device.info.wifi': 'WiFi',
'device.info.uptime': 'Uptime',
'device.info.android_version': 'Android Version',
'device.info.app_version': 'App Version',
'device.info.screen_resolution': 'Screen Resolution',
'device.info.ram': 'RAM',
'device.info.cpu_usage': 'CPU Usage',
// Uptime timeline
'device.timeline.title': 'Uptime Timeline (Last 24 Hours)',
'device.timeline.h24_ago': '24h ago',
'device.timeline.now': 'Now',
'device.timeline.online': 'Online',
'device.timeline.offline': 'Offline',
'device.timeline.no_data': 'No data',
'device.timeline.uptime_pct_tracked': '{pct}% uptime ({n}min tracked)',
'device.timeline.uptime_pct_no_data': '{pct}% uptime (no data)',
// Form
'device.form.orientation_label': 'Orientation / Rotation',
'device.form.orientation.landscape': 'Landscape (0°)',
'device.form.orientation.portrait': 'Portrait (90° CW)',
'device.form.orientation.landscape_flipped': 'Landscape Flipped (180°)',
'device.form.orientation.portrait_flipped': 'Portrait Flipped (270° CW)',
'device.form.default_content_label': 'Default Content',
'device.form.default_content_none': 'None (show "Waiting...")',
'device.form.notes_label': 'Notes',
'device.form.notes_placeholder': 'Location, setup details, etc.',
'device.debug.toggle': 'Debug logging (live)',
'device.debug.hint': 'Streams player/zone logs from this device in real time. Turns off on its own when the device reconnects.',
'device.form.save_settings': 'Save Settings',
// Control buttons
'device.ctl.reboot_device': 'Reboot Device',
'device.ctl.screen_off': 'Screen Off',
'device.ctl.screen_on': 'Screen On',
'device.ctl.launch_player': 'Launch Player',
'device.ctl.force_update': 'Force Update',
'device.ctl.shutdown': 'Shutdown',
// Remote tab
'device.remote.start_prompt': 'Click "Start Remote" to begin',
'device.remote.start': 'Start Remote',
'device.remote.stop': 'Stop Remote',
'device.remote.vol_up': 'Vol +',
'device.remote.vol_down': 'Vol -',
'device.remote.home': 'Home',
'device.remote.back': 'Back',
'device.remote.recents': 'Recents',
'device.remote.power': 'Power',
'device.remote.ok': 'OK',
'device.remote.settings': 'Settings',
'device.remote.scrn_off': 'Scrn Off',
'device.remote.scrn_on': 'Scrn On',
'device.remote.enable_system_view': 'Enable System View',
'device.remote.system_view_tooltip': 'Prompts the device user to allow full screen capture - enables remote view of home screen, settings, and other apps',
'device.remote.system_view_hint': 'Requires one-time approval on device',
'device.remote.waiting_for_approval': 'Waiting for device approval...',
'device.remote.system_view_enabled': 'System View Enabled',
'device.remote.unlocked_hint': 'Navigation and system controls unlocked',
// Playlist item
'device.pl_item.widget_with_type': 'Widget ({type})',
'device.pl_item.youtube': 'YouTube',
'device.pl_item.video': 'Video',
'device.pl_item.image': 'Image',
'device.pl_item.zone_label': 'Zone: {id}',
'device.pl_item.no_zone': 'No zone',
'device.pl_item.mute': 'Mute',
'device.pl_item.unmute': 'Unmute',
'device.pl_item.remove': 'Remove',
// Copy playlist
'device.copy.no_other_devices': 'No other devices to copy to',
'device.copy.prompt': 'Copy playlist to which device?\n\n{list}\n\nEnter number:',
'device.copy.invalid_selection': 'Invalid selection',
'device.copy.toast': 'Copied {n} items to {device}',
// Add-content modal
'device.assign.empty_all': 'No content, widgets, or kiosk pages yet. Create something first!',
'device.assign.modal_title': 'Add to Playlist',
'device.assign.zone_label': 'Zone',
'device.assign.zone_default': 'Default (fullscreen)',
'device.assign.zone_no_layout': 'This device has no layout assigned. Content will play fullscreen. Pick a layout from the Layout dropdown on this device to use zones.',
'device.assign.zone_load_failed': 'Layout zones could not be loaded. Try refreshing the page.',
'device.assign.zone_empty_layout': 'This layout has no zones defined.',
'device.assign.duration_label': 'Display Duration (seconds, for images/widgets)',
'device.assign.tab.media': 'Media ({n})',
'device.assign.tab.widgets': 'Widgets ({n})',
'device.assign.tab.kiosk': 'Kiosk ({n})',
'device.assign.no_media': 'No media uploaded yet',
'device.assign.no_widgets': 'No widgets created yet.',
'device.assign.no_kiosk': 'No kiosk pages yet.',
'device.assign.create_one': 'Create one',
'device.assign.add_selected': 'Add Selected',
'device.assign.select_first': 'Select something first',
'device.assign.kiosk_widget_name': 'Kiosk: {name}',
// Toasts
'device.toast.screenshot_requested': 'Screenshot requested',
'device.toast.renamed': 'Display renamed',
'device.toast.removing': 'Removing...',
'device.toast.removed': 'Display removed',
'device.toast.settings_saved': 'Settings saved',
'device.toast.published': 'Playlist published — devices updated',
'device.toast.draft_discarded': 'Draft changes discarded',
'device.toast.playlist_changed': 'Playlist changed',
'device.toast.layout_applied': 'Layout applied',
'device.toast.switched_to_fullscreen': 'Switched to fullscreen',
'device.toast.added_to_playlist': 'Added to playlist',
'device.toast.unmuted': 'Unmuted',
'device.toast.muted': 'Muted',
'device.toast.zone_updated': 'Zone updated',
'device.toast.removed_from_playlist': 'Content removed from playlist',
'device.toast.playlist_reordered': 'Playlist reordered',
'device.toast.reboot_sent': 'Reboot command sent',
'device.toast.shutdown_sent': 'Shutdown command sent',
'device.toast.screen_off_sent': 'Screen off command sent',
'device.toast.screen_on_sent': 'Screen on command sent',
'device.toast.launch_sent': 'Launch command sent',
'device.toast.update_triggered': 'Update check triggered',
'device.toast.remote_started': 'Remote session started',
'device.toast.command_queued': '{cmd} — device offline, will deliver on reconnect',
'device.toast.command_undeliverable': '{cmd} — device offline and queue unavailable',
'device.toast.command_no_ack': '{cmd} — no server response',
// Settings
'settings.title': 'Settings',
'settings.subtitle': 'Server configuration and setup information',
'settings.account': 'Account',
'settings.save_profile': 'Save Profile',
'settings.email_alerts': 'Email me when devices go offline',
'settings.change_password': 'Change Password',
'settings.password_min_8': 'Must be at least 8 characters.',
'settings.current_password': 'Current Password',
'settings.new_password': 'New Password',
'settings.confirm_new_password': 'Confirm New Password',
'settings.sso_note': 'You sign in via {provider}. Manage your password there.',
'settings.license': 'License',
'settings.license_mit': 'MIT License - all features included.',
'settings.platform_admin_link': 'Platform admin tools are in the',
'settings.platform_admin_page_suffix': 'page.',
'settings.user_management': 'User Management',
'settings.loading_users': 'Loading users...',
'settings.white_label': 'White Label / Branding',
'settings.white_label_desc': 'Customize the look of your dashboard and player for your clients.',
'settings.brand_name': 'Brand Name',
'settings.logo_url': 'Logo URL',
'settings.primary_color': 'Primary Color',
'settings.bg_color': 'Background Color',
'settings.custom_domain': 'Custom Domain',
'settings.favicon_url': 'Favicon URL',
'settings.custom_css': 'Custom CSS (optional)',
'settings.hide_branding': 'Hide "ScreenTinker" branding',
'settings.save_branding': 'Save Branding',
'settings.preview': 'Preview',
'settings.white_label_enterprise_only': 'Custom branding is available on the Enterprise plan',
'settings.view_plans': 'View Plans',
'settings.server_info': 'Server Information',
'settings.server_url': 'Server URL',
'settings.api_endpoint': 'API Endpoint',
'settings.server_url_hint': 'Use this URL when setting up the Android app',
'settings.setup_guide': 'Setup Guide',
'settings.setup_step_1': 'Install the ScreenTinker APK on your TV via sideloading',
'settings.setup_step_2_prefix': 'Open the app and enter this server URL:',
'settings.setup_step_3': 'The app will display a 6-digit pairing code',
'settings.setup_step_4': 'Click "Add Display" on the dashboard and enter the pairing code',
'settings.setup_step_5': 'Upload content in the Content Library',
'settings.setup_step_6': "Assign content to the display's Playlist",
'settings.your_data': 'Your Data',
'settings.your_data_desc': 'Export or import your devices, content, layouts, schedules, and all settings. Use this to migrate between cloud and self-hosted instances.',
'settings.export_my_data': 'Export My Data',
'settings.include_media_zip': 'Include media files (ZIP)',
'settings.import_data': 'Import Data',
'settings.language': 'Language',
'settings.about': 'About',
'settings.about_tagline': 'Digital signage management system.',
'settings.third_party_licenses': 'Third-Party Licenses',
// Import flow
'settings.import.reading_file': 'Reading file...',
'settings.import.zip_detected': 'ZIP export detected: <strong>{name}</strong> ({size} MB)<br>Contains data + media files.',
'settings.import.confirm': 'Confirm Import',
'settings.import.invalid_file': 'Invalid file. Must be a ScreenTinker export JSON or ZIP.',
'settings.import.summary_devices': '{n} devices',
'settings.import.summary_content': '{n} content items',
'settings.import.summary_widgets': '{n} widgets',
'settings.import.summary_layouts': '{n} layouts',
'settings.import.summary_schedules': '{n} schedules',
'settings.import.summary_walls': '{n} video walls',
'settings.import.summary_kiosk': '{n} kiosk pages',
'settings.import.found_summary': 'Found: {summary}.<br>From: {email} (exported {date})',
'settings.import.empty_export': 'empty export',
'settings.import.uploading_zip': 'Uploading and importing... This may take a moment for large files.',
'settings.import.importing': 'Importing...',
'settings.import.complete': 'Import complete: {imported}.',
'settings.import.pairing_codes_title': 'Device Pairing Codes:',
'settings.import.pairing_codes_hint': 'Enter these codes on each device to re-link them. All assignments and schedules will be preserved.',
'settings.import.failed': 'Import failed',
'settings.import.failed_with_error': 'Import failed: {error}',
'settings.import.read_failed': 'Failed to read file: {error}',
// Settings toasts
'settings.toast.support_token_generated': 'Support token generated (valid {hours}h)',
'settings.toast.import_success': 'Data imported successfully',
'settings.toast.name_required': 'Name cannot be empty',
'settings.toast.profile_saved': 'Profile saved',
'settings.toast.current_password_required': 'Enter your current password',
'settings.toast.new_password_min_8': 'New password must be at least 8 characters',
'settings.toast.passwords_dont_match': 'New passwords do not match',
'settings.toast.password_changed': 'Password changed',
'settings.toast.branding_saved': 'Branding saved',
'settings.toast.preview_applied': 'Preview applied (refresh to reset)',
'settings.toast.plan_updated': 'Plan updated',
'settings.toast.user_removed': 'User removed',
// User management table
'settings.user.col_user': 'User',
'settings.user.col_auth': 'Auth',
'settings.user.col_role': 'Role',
'settings.user.col_plan': 'Plan',
'settings.user.col_actions': 'Actions',
'settings.user.remove': 'Remove',
'settings.user.you': 'You',
'settings.user.confirm': 'Confirm?',
'settings.user.count_one': '1 user registered',
'settings.user.count_other': '{n} users registered',
'settings.user.reset_password': 'Reset Password',
'settings.user.prompt_reset_password': 'Enter a new password for {email} (minimum 8 characters):',
'settings.toast.password_reset_for_user': 'Password reset',
// Widgets
'widget.title': 'Widgets',
'widget.subtitle': 'Add dynamic content to your layouts',
'widget.help_tip': 'Dynamic content elements: live clocks, weather, RSS tickers, text, webpages, and social feeds. Create a widget then assign it to a device playlist.',
'widget.new_widget': 'New Widget',
'widget.configure': 'Configure Widget',
'widget.preview': 'Preview',
'widget.preview_title': 'Preview',
'widget.close': 'Close',
'widget.edit_x': 'Edit {type}',
'widget.new_x': 'New {type}',
'widget.empty_title': 'No widgets yet',
'widget.empty_desc': 'Create a widget to add dynamic content to your layouts.',
'widget.this_widget': 'this widget',
'widget.confirm_delete': 'Delete "{name}"? This cannot be undone.',
'widget.toast.saved': 'Widget saved',
'widget.toast.deleted': 'Widget deleted',
'widget.toast.preview_failed': 'Preview failed',
// Widget types
'widget.type.clock.name': 'Clock',
'widget.type.clock.desc': 'Digital clock with date',
'widget.type.weather.name': 'Weather',
'widget.type.weather.desc': 'Current weather conditions',
'widget.type.rss.name': 'News Ticker',
'widget.type.rss.desc': 'Scrolling RSS feed',
'widget.type.text.name': 'Text/HTML',
'widget.type.text.desc': 'Custom text or HTML content',
'widget.type.webpage.name': 'Webpage',
'widget.type.webpage.desc': 'Embed a webpage',
'widget.type.social.name': 'Social Feed',
'widget.type.social.desc': 'Social media feed',
'widget.type.directory_board.name': 'Directory Board',
'widget.type.directory_board.desc': 'Scrolling tenant/room directory for lobbies',
// Widget config form fields
'widget.field.name': 'Widget Name',
'widget.field.format': 'Format',
'widget.field.format_12h': '12 Hour',
'widget.field.format_24h': '24 Hour',
'widget.field.timezone': 'Timezone',
'widget.field.font_size': 'Font Size',
'widget.field.font_size_px': 'Font Size (px)',
'widget.field.color': 'Color',
'widget.field.background': 'Background',
'widget.field.location': 'Location',
'widget.field.location_placeholder': 'City, State',
'widget.field.units': 'Units',
'widget.field.units_imperial': 'Imperial (°F)',
'widget.field.units_metric': 'Metric (°C)',
'widget.field.feed_url': 'Feed URL',
'widget.field.scroll_speed_seconds': 'Scroll Speed (seconds)',
'widget.field.max_items': 'Max Items',
'widget.field.html_content': 'HTML Content',
'widget.field.css_optional': 'CSS (optional)',
'widget.field.url': 'URL',
'widget.field.zoom_pct': 'Zoom (%)',
'widget.field.refresh_interval': 'Refresh Interval (seconds, 0 = never)',
'widget.field.platform': 'Platform',
'widget.field.platform_twitter': 'Twitter/X',
'widget.field.platform_instagram': 'Instagram',
'widget.field.query': 'Query',
'widget.field.query_placeholder': '@handle or #hashtag',
// Content picker
'widget.picker.default_title': 'Select Image',
'widget.picker.select_logo': 'Select Logo',
'widget.picker.select_bg_images': 'Select Background Images',
'widget.picker.search': 'Search images...',
'widget.picker.no_matches': 'No matches.',
'widget.picker.no_images': 'No images in your content library. Upload images first from Content Library.',
'widget.picker.selected_count': '{n} selected',
// Directory Board
'widget.dir.title_label': 'Title',
'widget.dir.title_placeholder': 'Lincoln Warehouse',
'widget.dir.logo_label': 'Logo (optional)',
'widget.dir.footer_text_label': 'Footer Text',
'widget.dir.footer_placeholder': 'For Leasing Inquiries: Contact...',
'widget.dir.bg_images_label': 'Background Images (optional)',
'widget.dir.bg_images_hint': 'Images crossfade every 15 seconds at 30% opacity. Add multiple for rotation.',
'widget.dir.add_bg_image': '+ Add Background Image',
'widget.dir.theme': 'Theme',
'widget.dir.theme_dark': 'Dark',
'widget.dir.theme_light': 'Light',
'widget.dir.scroll_speed': 'Scroll Speed',
'widget.dir.speed_slow': 'Slow',
'widget.dir.speed_medium': 'Medium',
'widget.dir.speed_fast': 'Fast',
'widget.dir.columns': 'Columns',
'widget.dir.columns_auto': 'Auto',
'widget.dir.categories': 'Categories',
'widget.dir.add_category': '+ Add Category',
'widget.dir.add_entry': '+ Add Entry',
'widget.dir.empty_categories': 'Add your first floor or department to get started',
'widget.dir.no_entries': 'No entries yet',
'widget.dir.entry': 'entry',
'widget.dir.entries': 'entries',
'widget.dir.collapse': 'Collapse',
'widget.dir.expand': 'Expand',
'widget.dir.move_up': 'Move up',
'widget.dir.move_down': 'Move down',
'widget.dir.delete_category': 'Delete category',
'widget.dir.delete_entry': 'Delete entry',
'widget.dir.unnamed': '(unnamed)',
'widget.dir.confirm_delete_category': 'Delete category "{name}" and all its entries?',
'widget.dir.category_name_placeholder': 'e.g. First Floor',
'widget.dir.entry_id_placeholder': '101',
'widget.dir.entry_name_placeholder': 'Tenant name',
'widget.dir.entry_subtitle_placeholder': 'Details (optional)',
'widget.dir.available': 'Available',
'widget.dir.change': 'Change',
'widget.dir.choose_logo': 'Choose Logo',
'widget.dir.remove_logo': 'Remove',
'widget.dir.no_bg_images': 'No background images selected',
'widget.dir.remove_bg': 'Remove',
// Designer
'designer.title': 'Content Designer',
'designer.subtitle': 'Create dynamic signage content',
'designer.ai.title': '✨ AI generate',
'designer.ai.settings': 'AI settings',
'designer.ai.placeholder': "Describe your sign — e.g. 'Summer sale, 20% off mains, bright modern'",
'designer.ai.generate': 'Generate design',
'designer.ai.generating': 'Generating…',
'designer.ai.contacting': 'Generating… text is quick; images add ~1030s',
'designer.ai.done': 'Generated {n} element(s) — tweak and Publish.',
'designer.ai.done_imgwarn': 'Generated {n} element(s) — an image couldnt be generated (text is ready). Publish.',
'designer.ai.images_title': 'AI images (optional)',
'designer.ai.images_desc': 'Generate a background (and a foreground graphic) from your prompt. Point at a local sd.cpp / ComfyUI server or OpenAI. Off = text + shapes only.',
'designer.ai.image_provider': 'Image provider',
'designer.ai.image_off': 'Off (text + shapes only)',
'designer.ai.image_base_url': 'Image endpoint URL',
'designer.ai.image_model': 'Image model',
'designer.ai.image_model_ph': 'optional — e.g. dall-e-3; blank for sd.cpp / ComfyUI',
'designer.ai.failed': 'Generation failed',
'designer.ai.need_prompt': 'Enter a prompt first',
'designer.ai.settings_title': 'AI design settings',
'designer.ai.settings_desc': 'Bring your own OpenAI-compatible endpoint — OpenAI cloud, or self-hosted Ollama / LM Studio. Your provider bills you; the key is stored encrypted and never shown again.',
'designer.ai.base_url': 'Endpoint base URL',
'designer.ai.model': 'Model',
'designer.ai.load_models': 'Load models',
'designer.ai.loading_models': 'Loading models…',
'designer.ai.models_loaded': 'Loaded {n} model(s) — pick one above',
'designer.ai.models_failed': 'Could not load models',
'designer.ai.need_base_url': 'Enter the endpoint URL first',
'designer.ai.api_key': 'API key',
'designer.ai.key_set': '•••••• saved — leave blank to keep',
'designer.ai.key_placeholder': 'leave blank if your endpoint needs none',
'designer.ai.key_hint': 'Stored encrypted, server-side. Local endpoints (Ollama) usually need no key.',
'designer.ai.saved': 'AI settings saved',
'designer.ai.save_failed': 'Could not save AI settings',
'designer.help_tip': 'Create custom signage with live elements: clocks, weather, RSS tickers, countdowns, QR codes. Publish as a widget or export as PNG.',
'designer.load_design': 'Load Design',
'designer.export_png': 'Export PNG',
'designer.publish': 'Publish to Library',
'designer.preview_hint': 'Click elements to select. Drag to reposition. Live preview updates in real-time.',
'designer.add_element': 'Add Element',
'designer.background': 'Background',
'designer.bg_image': 'Image',
'designer.properties': 'Properties',
'designer.layers': 'Layers',
'designer.no_elements': 'No elements yet',
'designer.save_design_file': 'Save Design File',
'designer.qr_label': 'QR CODE',
'designer.loading_news': 'Loading news...',
'designer.no_items': 'No items',
'designer.feed_unavailable': 'Feed unavailable',
'designer.countdown_now': 'NOW!',
'designer.widget_name': 'Design {date}',
// Element buttons
'designer.el.text': 'Text',
'designer.el.heading': 'Heading',
'designer.el.image': 'Image',
'designer.el.video': 'Video',
'designer.el.clock': 'Clock',
'designer.el.date': 'Date',
'designer.el.weather': 'Weather',
'designer.el.ticker': 'Ticker',
'designer.el.shape': 'Shape',
'designer.el.qr': 'QR Code',
'designer.el.countdown': 'Countdown',
'designer.el.webpage': 'Webpage',
// Backgrounds
'designer.bg.black': 'Black',
'designer.bg.dark_blue': 'Dark Blue',
'designer.bg.dark_gradient': 'Dark Gradient',
'designer.bg.blue_gradient': 'Blue Gradient',
'designer.bg.sunset': 'Sunset',
'designer.bg.ocean': 'Ocean',
'designer.bg.forest': 'Forest',
'designer.bg.dark_red': 'Dark Red',
'designer.bg.white': 'White',
// Defaults / prompts
'designer.default.text': 'Your text here',
'designer.default.heading': 'HEADING',
'designer.default.coming_soon': 'Coming Soon',
'designer.prompt.video_url': 'Video URL (MP4):',
'designer.prompt.weather_location': 'City, State:',
'designer.prompt.rss_url': 'RSS Feed URL:',
'designer.prompt.qr_url': 'QR Code URL:',
'designer.prompt.countdown_date': 'Target date (YYYY-MM-DD):',
'designer.prompt.webpage_url': 'Webpage URL:',
// Properties
'designer.prop.text': 'Text',
'designer.prop.size': 'Size',
'designer.prop.font': 'Font',
'designer.prop.color': 'Color',
'designer.prop.bold': 'Bold',
'designer.prop.shadow': 'Shadow',
'designer.prop.format': 'Format',
'designer.prop.show_seconds': 'Show seconds',
'designer.prop.muted': 'Muted',
'designer.prop.loop': 'Loop',
'designer.prop.opacity': 'Opacity',
'designer.prop.shape': 'Shape',
'designer.prop.location': 'Location',
'designer.prop.feed_url': 'Feed URL',
'designer.prop.speed': 'Speed (seconds)',
'designer.prop.text_color': 'Text Color',
'designer.prop.bg_color': 'BG Color',
'designer.prop.target_date': 'Target Date',
'designer.prop.label': 'Label',
// Toasts
'designer.toast.published': 'Published as widget! Assign it to a layout zone.',
'designer.toast.publish_failed': 'Publish failed',
'designer.toast.export_failed': 'Export failed: {error}',
'designer.toast.loaded': 'Design loaded',
'designer.toast.invalid_file': 'Invalid design file',
// Playlists
'playlist.title': 'Playlists',
'playlist.subtitle': 'Create and manage content playlists',
'playlist.show_auto_generated': 'Show auto-generated',
'playlist.new_playlist_btn': '+ New Playlist',
'playlist.new_playlist': 'New Playlist',
'playlist.empty_title': 'No playlists yet',
'playlist.empty_desc': 'Create your first playlist to organize content for your displays.',
'playlist.all_auto_generated': 'All playlists are auto-generated. Toggle "Show auto-generated" to see them.',
'playlist.tag_auto': 'auto',
'playlist.tag_draft': 'draft',
'playlist.item_count_one': '1 item',
'playlist.item_count_other': '{n} items',
'playlist.created_at': 'Created {date}',
'playlist.display_count_one': '1 display',
'playlist.display_count_other': '{n} displays',
'playlist.assigned_to_one': 'Assigned to 1 display',
'playlist.assigned_to_other': 'Assigned to {n} displays',
'playlist.load_failed': 'Failed to load playlists: {error}',
'playlist.back_to_playlists': 'Back to Playlists',
'playlist.name_placeholder': 'Playlist name',
'playlist.desc_placeholder': 'Description (optional)',
'playlist.create_btn': 'Create',
'playlist.add_desc_placeholder': 'Add a description...',
'playlist.click_to_rename': 'Click to rename',
'playlist.click_to_edit_desc': 'Click to edit description',
'playlist.add_content': '+ Add Content',
'playlist.delete_playlist': 'Delete Playlist',
'playlist.back': 'Back',
'playlist.items_empty': 'This playlist is empty',
'playlist.items_empty_hint': 'Click "Add Content" to add items.',
'playlist.duration': 'Duration',
'playlist.sec': 'sec',
'playlist.move_up': 'Move up',
'playlist.move_down': 'Move down',
'playlist.remove_item': 'Remove item',
'playlist.item_widget': 'Widget',
'playlist.unknown_type': 'Unknown type',
'playlist.confirm_delete': 'Delete "{name}"? This cannot be undone.',
'playlist.confirm_discard_draft': 'Discard all unpublished changes and revert to the last published version?',
'playlist.draft.banner_title': 'Unpublished changes',
'playlist.draft.devices_showing_published': 'Devices are still showing the last published version.',
'playlist.draft.never_published': 'This playlist has never been published. Devices will show nothing until you publish.',
'playlist.draft.discard_changes': 'Discard Changes',
'playlist.draft.publish': 'Publish',
'playlist.draft.publishing': 'Publishing...',
'playlist.toast.created': 'Playlist created',
'playlist.toast.deleted': 'Playlist deleted',
'playlist.toast.published': 'Playlist published — devices updated',
'playlist.toast.draft_discarded': 'Draft changes discarded',
'playlist.toast.item_removed': 'Item removed',
'playlist.add_modal_title': 'Add Content to Playlist',
'playlist.tab_content': 'Content',
'playlist.tab_widgets': 'Widgets',
'playlist.search_placeholder': 'Search...',
'playlist.close': 'Close',
'playlist.no_content_found': 'No content found',
'playlist.no_widgets_found': 'No widgets found',
'playlist.add_btn': 'Add',
'playlist.adding': 'Adding...',
'playlist.added': 'Added',
// Onboarding
'onboarding.back': 'Back',
'onboarding.next': 'Next',
'onboarding.skip': 'Skip Wizard',
'onboarding.go_to_dashboard': 'Go to Dashboard',
'onboarding.pair_display': 'Pair Display',
'onboarding.step.welcome.title': 'Welcome to ScreenTinker!',
'onboarding.step.welcome.intro': "Let's get you set up in under 5 minutes.",
'onboarding.step.welcome.guide_through': 'This wizard will guide you through:',
'onboarding.step.welcome.bullet_download': 'Downloading the player app',
'onboarding.step.welcome.bullet_pair': 'Pairing your first display',
'onboarding.step.welcome.bullet_upload': 'Uploading and assigning content',
'onboarding.step.player.title': 'Step 1: Get the Player App',
'onboarding.step.player.intro': 'Install the player on your display device.',
'onboarding.step.player.android_label': 'Android APK',
'onboarding.step.player.android_desc': 'TV boxes, tablets, Fire TV',
'onboarding.step.player.web_label': 'Web Player',
'onboarding.step.player.web_desc': 'Any browser, Pi, ChromeOS',
'onboarding.step.player.url_hint': 'Open the app on your display and enter this server URL:',
'onboarding.step.pair.title': 'Step 2: Pair Your Display',
'onboarding.step.pair.intro': 'Enter the 6-digit code shown on your display.',
'onboarding.step.pair.name_placeholder': 'Display name (e.g., Lobby TV)',
'onboarding.step.upload.title': 'Step 3: Upload Content',
'onboarding.step.upload.intro': 'Upload a video or image to display.',
'onboarding.step.upload.click_to_select': 'Click to select a file',
'onboarding.step.upload.formats': 'MP4, WebM, JPEG, PNG, GIF',
'onboarding.step.upload.uploading': 'Uploading...',
'onboarding.step.done.title': "You're All Set!",
'onboarding.step.done.intro': 'Your display is paired and content is playing!',
'onboarding.step.done.whats_next': "What's next?",
'onboarding.step.done.next_content': 'Add more content in the <strong>Content Library</strong>',
'onboarding.step.done.next_layouts': 'Create multi-zone layouts in <strong>Layouts</strong>',
'onboarding.step.done.next_schedule': 'Set up a schedule in the <strong>Schedule</strong> calendar',
'onboarding.step.done.next_widgets': 'Add live widgets (clock, weather, ticker) in <strong>Widgets</strong>',
'onboarding.step.done.next_kiosk': 'Create interactive screens in <strong>Kiosk</strong>',
'onboarding.step.done.next_designer': 'Design custom content in the <strong>Designer</strong>',
'onboarding.toast.invalid_code': 'Enter a valid 6-digit code',
'onboarding.toast.pairing': 'Pairing...',
'onboarding.toast.pair_failed': 'Pairing failed',
'onboarding.toast.pair_failed_with_error': 'Pairing failed: {error}',
'onboarding.toast.paired': 'Display paired!',
'onboarding.toast.uploaded_assigning': 'Uploaded! Assigning to display...',
'onboarding.toast.content_assigned': 'Content uploaded and assigned!',
'onboarding.toast.upload_failed': 'Upload failed',
'onboarding.toast.error_with_error': 'Error: {error}',
// Admin (platform admin panel)
'admin.title': 'Platform Admin',
'admin.subtitle': 'Superadmin controls - only you can see this',
'admin.add_user': 'Add user',
'admin.create_org.button': 'Create organization',
'admin.create_org.title': 'Create organization',
'admin.create_org.name': 'Organization name',
'admin.create_org.placeholder': 'e.g. Acme Corp',
'admin.create_org.hint': "Creates the organization and its first workspace, owned by you. Add the customer's users afterward with Add User.",
'admin.create_org.submit': 'Create',
'admin.create_org.success': 'Organization "{name}" created',
'admin.create_org.err_empty': 'Organization name cannot be empty',
'admin.create_org.err_failed': 'Could not create organization',
'admin.orgs.title': 'Organizations',
'admin.orgs.desc': 'Every organization and its workspaces. Deleting cascades all devices, content, playlists and memberships — this is irreversible.',
'admin.orgs.empty': 'No organizations yet.',
'admin.orgs.owner': 'Owner',
'admin.orgs.workspaces': 'workspaces',
'admin.orgs.devices': 'devices',
'admin.orgs.members': 'members',
'admin.orgs.delete_org': 'Delete org',
'admin.orgs.delete_ws': 'Delete',
'admin.orgs.delete_org_title': 'Delete organization',
'admin.orgs.delete_org_body': 'This permanently deletes <b>{name}</b> and all of its workspaces, devices, content, playlists and memberships. This cannot be undone.',
'admin.orgs.delete_ws_title': 'Delete workspace',
'admin.orgs.delete_ws_body': 'This permanently deletes workspace <b>{name}</b> and all of its devices, content and playlists. The organization is kept. This cannot be undone.',
'admin.orgs.org_deleted': 'Organization "{name}" deleted',
'admin.orgs.ws_deleted': 'Workspace "{name}" deleted',
'admin.access_denied': 'Access Denied',
'admin.access_denied_desc': 'Platform admin access required.',
'admin.all_users': 'All Users',
'admin.plans': 'Subscription Plans',
'admin.system': 'System',
// #15: instance-level default branding
'admin.branding.title': 'Default branding',
'admin.branding.desc': "Instance-wide default. Every workspace that hasn't set its own white-label inherits this, as does the login page.",
'admin.branding.brand_name': 'Brand name',
'admin.branding.primary_color': 'Primary color',
'admin.branding.bg_color': 'Background color',
'admin.branding.logo_url': 'Logo URL',
'admin.branding.favicon_url': 'Favicon URL',
'admin.branding.custom_css': 'Custom CSS',
'admin.branding.hide_branding': 'Hide "Powered by" branding',
'admin.branding.save': 'Save branding',
'admin.branding.saved': 'Default branding saved',
'admin.col.user': 'User',
'admin.col.auth': 'Auth',
'admin.col.last_login': 'Last Login',
'admin.col.role': 'Role',
'admin.col.plan': 'Plan',
'admin.col.workspace': 'Workspace',
'admin.col.actions': 'Actions',
'admin.workspace.unassigned': 'Unassigned',
'admin.workspace.multi': '{n} workspaces',
'admin.workspace.platform_all': 'Platform (all)',
'admin.workspace.manage': 'Manage',
// "Manage workspaces" modal (per-user membership management)
'manage_ws.title': 'Manage workspaces — {user}',
'manage_ws.staff_note': 'This user has platform-wide access; the memberships below are in addition to that.',
'manage_ws.current': 'Current workspaces',
'manage_ws.empty': 'Not a member of any workspace.',
'manage_ws.add': 'Add to workspace',
'manage_ws.filter': 'Filter workspaces…',
'manage_ws.pick': 'Select a workspace…',
'manage_ws.pick_required': 'Pick a workspace to add.',
'manage_ws.add_btn': 'Add',
'manage_ws.remove': 'Remove',
'manage_ws.done': 'Done',
'manage_ws.toast.added': 'Added to workspace',
'manage_ws.toast.removed': 'Removed from workspace',
'manage_ws.toast.role': 'Role updated',
'admin.col.devices': 'Devices',
'admin.col.storage': 'Storage',
'admin.col.monthly': 'Monthly',
'admin.col.yearly': 'Yearly',
'admin.role.user': 'User',
'admin.role.platform_operator': 'Platform operator',
'admin.role.platform_admin': 'Platform admin',
// Legacy labels kept for back-compat with any not-yet-normalized data; the
// role dropdown no longer offers these (#14 normalization).
'admin.role.admin': 'Admin',
'admin.role.superadmin': 'Superadmin',
'admin.remove': 'Remove',
'admin.owner': 'Owner',
'admin.confirm': 'Confirm?',
'admin.total_users': '{n} total users',
'admin.unlimited': 'Unlimited',
'admin.free': 'Free',
'admin.version': 'Version',
'admin.frontend_hash': 'Frontend Hash',
'admin.download_db_backup': 'Download DB Backup',
'admin.server_status': 'Server Status',
'admin.toast.role_updated': 'Role updated',
'admin.toast.plan_updated': 'Plan updated',
'admin.toast.workspace_updated': 'Workspace updated',
'admin.toast.user_removed': 'User removed',
'admin.reset_password': 'Reset Password',
'admin.prompt_reset_password': 'Enter a new password for {email} (minimum 8 characters):',
'admin.toast.password_min_8': 'Password must be at least 8 characters',
'admin.toast.password_reset': 'Password reset',
// Schedule
'schedule.title': 'Schedule',
'schedule.subtitle': 'Content scheduling calendar',
'schedule.help_tip': 'Visual weekly calendar for content scheduling. Click Add Schedule to create time slots. Set recurrence for repeating content. Higher priority overrides lower. Device-level schedules override group-level.',
'schedule.prev_week': '< Prev',
'schedule.next_week': 'Next >',
'schedule.add_schedule': 'Add Schedule',
'schedule.edit_schedule': 'Edit Schedule',
'schedule.apply_to': 'Apply to',
'schedule.target_device': 'Device',
'schedule.target_group': 'Group',
'schedule.group_devices_count': '{n} devices',
'schedule.no_groups_msg': 'No groups created yet. Create groups in the Displays page.',
'schedule.zone_note': 'Note: Zone-based schedules are layout-specific. Ensure all devices in the group use the same layout.',
'schedule.playlist_override': 'Playlist override',
'schedule.no_playlist_override': '— No playlist override —',
'schedule.draft_suffix': '(draft)',
'schedule.layout_override': 'Layout override',
'schedule.no_layout_override': '— No layout override —',
'schedule.content_label': 'Content',
'schedule.content_hint': '(single item, optional)',
'schedule.content_none': '— None —',
'schedule.title_label': 'Title (optional)',
'schedule.title_placeholder': 'e.g., Morning Playlist',
'schedule.start_time': 'Start Time',
'schedule.end_time': 'End Time',
'schedule.repeat': 'Repeat',
'schedule.repeat_none': 'No repeat',
'schedule.repeat_daily': 'Daily',
'schedule.repeat_weekdays': 'Weekdays',
'schedule.repeat_weekends': 'Weekends',
'schedule.repeat_weekly': 'Weekly',
'schedule.priority': 'Priority',
'schedule.color': 'Color',
'schedule.scheduled_label': 'Scheduled',
'schedule.tooltip_group_prefix': 'Group: ',
'schedule.tooltip_priority': 'Priority: {n}',
'schedule.day.sun': 'Sun',
'schedule.day.mon': 'Mon',
'schedule.day.tue': 'Tue',
'schedule.day.wed': 'Wed',
'schedule.day.thu': 'Thu',
'schedule.day.fri': 'Fri',
'schedule.day.sat': 'Sat',
'schedule.hour_12am': '12am',
'schedule.hour_am': 'am',
'schedule.hour_12pm': '12pm',
'schedule.hour_pm': 'pm',
'schedule.toast.no_groups': 'No groups available. Create a group first.',
'schedule.toast.saved': 'Schedule saved',
// Reports
'report.title': 'Reports',
'report.subtitle': 'Proof-of-play analytics and device uptime',
'report.help_tip': 'Proof-of-play analytics. See what played, when, and on which device. Filter by date range and device. Export to CSV for ad verification.',
'report.export_csv': 'Export CSV',
'report.device': 'Device',
'report.all_devices': 'All Devices',
'report.start_date': 'Start Date',
'report.end_date': 'End Date',
'report.load_report': 'Load Report',
'report.select_range': 'Select a date range and click Load Report',
'report.error': 'Error',
'report.total_plays': 'Total Plays',
'report.total_hours': 'Total Hours',
'report.unique_content': 'Unique Content',
'report.active_devices': 'Active Devices',
'report.avg_duration': 'Avg Duration',
'report.plays_per_day': 'Plays per Day',
'report.plays_by_hour': 'Plays by Hour',
'report.top_content': 'Top Content',
'report.by_device': 'By Device',
'report.no_data': 'No data',
'report.col.content': 'Content',
'report.col.device': 'Device',
'report.col.plays': 'Plays',
'report.col.total_hours': 'Total Hours',
'report.col.completion': 'Completion',
// Kiosk
'kiosk.title': 'Kiosk Pages',
'kiosk.subtitle': 'Create interactive touchscreen interfaces',
'kiosk.help_tip': 'Create interactive touchscreen interfaces. Add buttons with icons and actions. Includes idle screen that shows after inactivity. Assign to devices as a widget.',
'kiosk.new_page': 'New Kiosk Page',
'kiosk.prompt_name': 'Kiosk page name:',
'kiosk.empty_title': 'No kiosk pages yet',
'kiosk.empty_desc': 'Create an interactive touchscreen interface for your displays.',
'kiosk.label': 'Kiosk Page',
'kiosk.preview': 'Preview',
'kiosk.confirm_delete': 'Delete kiosk page "{name}"? This cannot be undone.',
'kiosk.toast.deleted': 'Kiosk page deleted',
'kiosk.toast.delete_failed': 'Failed to delete',
'kiosk.toast.saved': 'Kiosk page saved',
'kiosk.not_found': 'Page not found',
'kiosk.back': 'Back to Kiosk Pages',
'kiosk.page_settings': 'Page Settings',
'kiosk.title_label': 'Title',
'kiosk.subtitle_label': 'Subtitle',
'kiosk.logo_url': 'Logo URL',
'kiosk.footer_text': 'Footer Text',
'kiosk.idle_title': 'Idle Screen Title',
'kiosk.idle_default': 'Touch to Begin',
'kiosk.idle_timeout': 'Idle Timeout (seconds)',
'kiosk.style': 'Style',
'kiosk.background': 'Background',
'kiosk.text_color': 'Text Color',
'kiosk.columns': 'Columns',
'kiosk.button_color': 'Button Color',
'kiosk.button_hover': 'Button Hover Color',
'kiosk.buttons': 'Buttons',
'kiosk.add_btn': '+ Add',
'kiosk.icon_placeholder': 'Emoji',
'kiosk.label_placeholder': 'Label',
'kiosk.sublabel_placeholder': 'Sublabel',
'kiosk.action_none': 'No action',
'kiosk.action_url': 'Open URL',
'kiosk.action_page': 'Go to page',
'kiosk.url_placeholder': 'URL or page',
'kiosk.no_buttons': 'No buttons yet',
'kiosk.new_button': 'New Button',
// Layout editor
'layout.title': 'Layouts',
'layout.subtitle': 'Screen layouts and templates',
'layout.help_tip': 'Create multi-zone screen layouts. Use templates or build custom ones. Drag zones to position, resize with corner handle. Assign layouts to devices in the Playlist tab.',
'layout.new_layout': 'New Layout',
'layout.templates': 'Templates',
'layout.my_layouts': 'My Layouts',
'layout.empty_custom': 'No custom layouts yet',
'layout.prompt_name': 'Layout name:',
'layout.default_zone_name': 'Main',
'layout.template_label': 'Template',
'layout.use_template': 'Use Template',
'layout.zone_count_one': '1 zone',
'layout.zone_count_other': '{n} zones',
'layout.confirm_delete': 'Delete layout "{name}"? This cannot be undone.',
'layout.toast.deleted': 'Layout deleted',
'layout.toast.delete_failed': 'Failed to delete layout',
'layout.toast.saved': 'Layout saved',
'layout.not_found': 'Layout not found',
'layout.back': 'Back to Layouts',
'layout.add_zone': 'Add Zone',
'layout.zones': 'Zones',
'layout.properties': 'Properties',
'layout.delete_zone': 'Delete Zone',
'layout.zone_n': 'Zone {n}',
'layout.prop.name': 'Name',
'layout.prop.x': 'X (%)',
'layout.prop.y': 'Y (%)',
'layout.prop.width': 'Width (%)',
'layout.prop.height': 'Height (%)',
'layout.prop.type': 'Type',
'layout.type_content': 'Content',
'layout.type_widget': 'Widget',
'layout.prop.fit': 'Fit',
'layout.fit_contain': 'Contain (whole image, may letterbox)',
'layout.fit_cover': 'Cover (fill zone, may crop)',
'layout.fit_fill': 'Stretch (fill zone, may distort)',
'layout.fit_hint': 'How video/images scale to the zone. Contain shows the whole frame without cropping.',
// Video walls
'wall.title': 'Video Walls',
'wall.subtitle': 'Combine multiple displays into one large screen',
'wall.help_tip': 'Combine multiple displays into one large screen. Set grid size, drag devices into positions, adjust bezel compensation. Assign content to play across all devices.',
'wall.new_wall': 'New Video Wall',
'wall.prompt_name': 'Video wall name:',
'wall.empty_title': 'No video walls yet',
'wall.empty_desc': 'Create a video wall to combine multiple displays.',
'wall.grid_summary': '{cols}x{rows} grid • {n} devices',
'wall.not_found': 'Wall not found',
'wall.back': 'Back to Video Walls',
'wall.delete_wall': 'Delete Wall',
'wall.grid_config': 'Grid Configuration',
'wall.columns': 'Columns',
'wall.rows': 'Rows',
'wall.h_bezel': 'H Bezel (px)',
'wall.v_bezel': 'V Bezel (px)',
'wall.update': 'Update',
'wall.content': 'Content',
'wall.no_content': 'No content',
'wall.set_content': 'Set Content',
'wall.available_displays': 'Available Displays',
'wall.all_assigned': 'All devices assigned',
'wall.drop_here': 'Drop here',
'wall.toast.placed': '{name} placed at [{col},{row}]',
'wall.toast.grid_updated': 'Grid updated',
'wall.toast.content_updated': 'Content updated',
'wall.toast.deleted': 'Wall deleted',
// Billing
'billing.title': 'Subscription',
'billing.subtitle': 'Manage your plan and billing',
'billing.current_plan': 'Current Plan',
'billing.self_hosted': 'Self-Hosted',
'billing.trial_days_left': 'Trial - {n} days left',
'billing.trial_ends': 'Your {plan} trial ends in {n} days',
'billing.trial_after': "After the trial, you'll be moved to the Free plan (1 device). Upgrade now to keep all your devices and features.",
'billing.devices': 'Devices',
'billing.devices_lc': 'devices',
'billing.storage': 'Storage',
'billing.storage_lc': 'storage',
'billing.features': 'Features',
'billing.feat.remote_control': 'Remote Control',
'billing.feat.remote_urls': 'Remote URLs',
'billing.feat.priority_support': 'Priority Support',
'billing.available_plans': 'Available Plans',
'billing.current': 'Current',
'billing.unlimited': 'Unlimited',
'billing.free': 'Free',
'billing.per_month': '/mo',
'billing.yearly_save': 'or ${price}/year (save {pct}%)',
'billing.monthly': 'Monthly',
'billing.yearly': 'Yearly',
'billing.manage_subscription': 'Manage Subscription',
'billing.self_hosted_note': 'Self-hosted mode: plans can be assigned by admins without billing.',
'billing.failed_to_load': 'Failed to load',
'billing.toast.checkout_failed': 'Failed to start checkout: {error}',
'billing.toast.portal_failed': 'Failed to open billing portal: {error}',
'billing.toast.payment_success': 'Payment successful! Your plan has been upgraded.',
// Teams
'team.title': 'Teams',
'team.subtitle': 'Manage teams and shared access',
'team.help_tip': 'Create teams to share devices with other users. Owners manage the team, editors can change content/playlists, viewers can only monitor.',
'team.new_team': 'New Team',
'team.prompt_name': 'Team name:',
'team.empty_title': 'No teams yet',
'team.empty_desc': 'Create a team to share devices with other users.',
'team.your_role': 'Your role: {role}',
'team.member_count_one': '1 member',
'team.member_count_other': '{n} members',
'team.not_found': 'Team not found',
'team.back': 'Back to Teams',
'team.delete_team': 'Delete Team',
'team.members_count': 'Members ({n})',
'team.invite': '+ Invite',
'team.role_viewer': 'Viewer',
'team.role_editor': 'Editor',
'team.role_owner': 'Owner',
'team.remove': 'Remove',
'team.remove_from_team': 'Remove from team',
'team.no_members': 'No members yet',
'team.shared_devices': 'Shared Devices ({n})',
'team.add_device': '+ Add device...',
'team.no_devices': 'No devices shared with this team',
'team.prompt_email': 'Email address to invite:',
'team.prompt_role': 'Role (viewer, editor, or owner):',
'team.toast.invalid_role': 'Invalid role',
'team.toast.invitation_sent': 'Invitation sent',
'team.toast.role_updated': 'Role updated',
'team.toast.member_removed': 'Member removed',
'team.toast.device_added': 'Device added to team',
'team.toast.device_removed': 'Device removed from team',
'team.toast.deleted': 'Team deleted',
// Activity log
'activity.title': 'Activity Log',
'activity.subtitle': 'Audit trail of all actions',
'activity.load_more': 'Load More',
'activity.empty_title': 'No activity yet',
'activity.empty_desc': 'Actions will appear here as you use the system.',
'activity.system': 'System',
'activity.verb_created': 'created',
'activity.verb_updated': 'updated',
'activity.verb_deleted': 'deleted',
'activity.action_paired_device': 'paired a device',
'activity.action_added_remote_content': 'added remote content',
'activity.noun_content': 'content',
'activity.noun_device': 'device',
'activity.noun_playlist_assignment': 'playlist assignment',
'activity.noun_assignment': 'assignment',
'activity.noun_layout': 'layout',
'activity.noun_widget': 'widget',
'activity.noun_schedule': 'schedule',
'activity.noun_video_wall': 'video wall',
'activity.alert_device_offline': 'alert: device went offline',
// Help
'help.title': 'Help Center',
'help.subtitle': 'Quick guides and FAQ',
'help.faq': 'Frequently Asked Questions',
'help.shortcuts': 'Keyboard Shortcuts',
'help.shortcut_esc': 'Reset web player (on player page)',
'help.shortcut_f': 'Toggle fullscreen (web player)',
// Add Display modal (in index.html)
'add_display.title': 'Add Display',
'add_display.intro': 'Enter the 6-digit pairing code shown on the display.',
'add_display.pairing_code': 'Pairing Code',
'add_display.display_name': 'Display Name (optional)',
'add_display.name_placeholder': 'e.g., Lobby TV',
'add_display.need_player': 'Need a player app? Install one to get a pairing code:',
'add_display.android_apk': 'Android APK',
'add_display.web_player': 'Web Player',
'add_display.raspberry_pi': 'Raspberry Pi',
'add_display.windows': 'Windows',
'add_display.smart_tv_note': 'Smart TVs (LG/Samsung): open the built-in browser and navigate to <code style="background:var(--bg-input,#0f172a);padding:1px 4px;border-radius:3px">/player</code>',
'add_display.pair_btn': 'Pair Display',
// Workspace switcher (Phase 3 MVP). devices_count is the only count exposed
// today; matching pattern for users/playlists/etc. when those land later.
'switcher.devices_count_one': '1 device',
'switcher.devices_count_other': '{n} devices',
'switcher.no_devices': 'No devices',
// #16: searchable org/workspace switcher
'switcher.search_placeholder': 'Search organizations…',
'switcher.no_matches': 'No matches',
// Workspace members (Slice 2A - read-only listing; 2B adds mutation keys).
'members.title': 'Workspace members',
'members.loading': 'Loading...',
'members.section.direct': 'Members',
'members.section.via_org': 'Organization access',
'members.section.pending': 'Pending invites',
'members.col.name': 'Name',
'members.col.email': 'Email',
'members.col.role': 'Role',
'members.col.joined': 'Joined',
'members.role.workspace_admin': 'Admin',
'members.role.workspace_editor': 'Editor',
'members.role.workspace_viewer': 'Viewer',
'members.role.org_owner': 'Org owner',
'members.role.org_admin': 'Org admin',
'members.via_org_label': 'via organization',
'members.invited_label': 'Invited',
'members.invited_by': 'Invited by {email}',
'members.expires_in': 'Expires {when}',
'members.empty.members': 'No direct members yet.',
'members.empty.invites': 'No pending invites.',
'members.load_error': 'Failed to load members: {error}',
'members.workspace_not_found': 'Workspace not found or no access.',
// Mutation UI (Slice 2B): invite modal, action buttons, confirms, error
// toasts, success toasts. Grouped under five sub-namespaces for clarity.
// Modal — invite form
'members.modal.invite_title': 'Invite to {workspace}',
'members.modal.email_label': 'Email',
'members.modal.email_placeholder': 'user@example.com',
'members.modal.role_label': 'Role',
'members.modal.cancel': 'Cancel',
'members.modal.send': 'Send invite',
'members.modal.sending': 'Sending...',
// Modal — Add User form (#10, admin-provisioned account)
'members.modal.add_user_title': 'Add user to {workspace}',
'members.modal.add_user_title_generic': 'Add user',
// Add User picker mode (platform Users admin page): choose the target workspace.
'members.modal.workspace_label': 'Organization / Workspace',
'members.modal.workspace_filter_placeholder': 'Filter workspaces…',
'members.modal.workspace_placeholder': 'Select a workspace…',
'members.modal.workspace_loading': 'Loading workspaces…',
'members.modal.workspace_none': 'No workspaces available',
'members.modal.workspace_load_error': 'Failed to load workspaces',
'members.modal.workspace_required': 'Please select a workspace.',
'members.modal.name_label': 'Name',
'members.modal.name_placeholder': 'Full name (optional)',
'members.modal.password_label': 'Password',
'members.modal.password_placeholder': 'Set a password',
'members.modal.generate': 'Generate',
'members.modal.must_change_label': 'Require a password change on first login',
'members.modal.create': 'Create user',
'members.modal.creating': 'Creating...',
// Buttons — page header + per-row action affordances (titles double as
// ARIA labels for the icon-only buttons).
'members.button.invite': 'Invite member',
'members.button.add_user': 'Add user',
'members.button.remove': 'Remove member',
'members.button.cancel_invite': 'Cancel invite',
// Native confirm() text for destructive actions.
'members.confirm.remove_member': 'Remove {name} from this workspace?',
'members.confirm.cancel_invite': 'Cancel invite for {email}?',
// Errors mapped from server response text by mapMutationError().
'members.error.rate_limit': 'Invite rate limit reached. Try again later.',
'members.error.invite_exists': 'An invite for that email is already pending.',
'members.error.last_admin_demote': 'Cannot change role - this user is the only admin.',
'members.error.last_admin_remove': 'Cannot remove the last admin.',
'members.error.already_member': 'That user is already a member of this workspace.',
'members.error.invalid_email': 'Please enter a valid email address.',
'members.error.org_owner_remove': 'Cannot remove the organization owner.',
'members.error.email_send_failed': 'Email send failed. Try again.',
'members.error.user_exists': 'A user with that email already exists.',
'members.error.password_min_8': 'Password must be at least 8 characters.',
'members.error.mutation_generic': 'Action failed: {error}',
// Success toasts fired post-mutation.
'members.success.invite_sent': 'Invite sent to {email}',
'members.success.invite_cancelled': 'Invite cancelled',
'members.success.role_changed': 'Role updated',
'members.success.member_removed': '{name} removed',
'members.success.user_created': 'User {email} created',
// Forced first-login password change (#10). Shown when an admin-provisioned
// user (must_change_password) is routed to #/change-password.
'forcepw.title': 'Set a new password',
'forcepw.subtitle': 'Your account was set up by an administrator. Choose your own password to continue.',
'forcepw.current': 'Current password',
'forcepw.new': 'New password',
'forcepw.confirm': 'Confirm new password',
'forcepw.hint': 'At least 8 characters.',
'forcepw.submit': 'Set password',
'forcepw.submitting': 'Saving...',
'forcepw.success': 'Password updated',
'forcepw.error_required': 'Enter your current and new password.',
'forcepw.error_min8': 'Password must be at least 8 characters.',
'forcepw.error_mismatch': "Passwords don't match.",
'forcepw.error_generic': 'Could not update password. Try again.',
// No-workspace empty state (#12): shown to an org-less signed-in user.
'noworkspace.title': 'No workspaces yet',
'noworkspace.body': "Your account isn't part of any workspace yet. Ask your administrator to add you to one, then sign in again.",
'noworkspace.sign_out': 'Sign out',
// Accept-invite flow (Slice 2C). Toasts that fire post-accept on the
// dashboard. Error variants share one helper in app.js's mapAcceptError().
'accept.success': "You've joined {name}",
'accept.already_member': "You're already a member of {name}",
'accept.error.not_found': 'Invite no longer valid',
'accept.error.expired': 'This invite has expired - ask the admin for a new one',
'accept.error.wrong_account': 'This invite is for a different email address. Sign out and sign in with the right account.',
'accept.error.generic': 'Failed to accept invite. Try again or contact your admin.',
};