diff --git a/frontend/js/i18n/de.js b/frontend/js/i18n/de.js index be0aed9..01ffa8d 100644 --- a/frontend/js/i18n/de.js +++ b/frontend/js/i18n/de.js @@ -768,4 +768,125 @@ export default { 'admin.toast.role_updated': 'Rolle aktualisiert', 'admin.toast.plan_updated': 'Plan aktualisiert', 'admin.toast.user_removed': 'Benutzer entfernt', + + // Schedule + 'schedule.title': 'Zeitplan', + 'schedule.subtitle': 'Inhaltsplanungs-Kalender', + 'schedule.help_tip': 'Visueller Wochenkalender für Inhaltsplanung. Klicken Sie auf Zeitplan hinzufügen, um Zeitfenster zu erstellen. Wiederholungen für regelmäßige Inhalte. Höhere Priorität überschreibt niedrigere. Geräteebene überschreibt Gruppenebene.', + 'schedule.prev_week': '< Zurück', + 'schedule.next_week': 'Weiter >', + 'schedule.add_schedule': 'Zeitplan hinzufügen', + 'schedule.edit_schedule': 'Zeitplan bearbeiten', + 'schedule.apply_to': 'Anwenden auf', + 'schedule.target_device': 'Gerät', + 'schedule.target_group': 'Gruppe', + 'schedule.group_devices_count': '{n} Geräte', + 'schedule.no_groups_msg': 'Noch keine Gruppen erstellt. Erstellen Sie sie auf der Seite Bildschirme.', + 'schedule.zone_note': 'Hinweis: Zonenbasierte Zeitpläne sind layout-spezifisch. Stellen Sie sicher, dass alle Geräte der Gruppe dasselbe Layout verwenden.', + 'schedule.playlist_override': 'Playlist überschreiben', + 'schedule.no_playlist_override': '— Keine Playlist-Überschreibung —', + 'schedule.draft_suffix': '(Entwurf)', + 'schedule.layout_override': 'Layout überschreiben', + 'schedule.no_layout_override': '— Keine Layout-Überschreibung —', + 'schedule.content_label': 'Inhalt', + 'schedule.content_hint': '(einzelnes Element, optional)', + 'schedule.content_none': '— Keiner —', + 'schedule.title_label': 'Titel (optional)', + 'schedule.title_placeholder': 'z. B. Morgenplaylist', + 'schedule.start_time': 'Startzeit', + 'schedule.end_time': 'Endzeit', + 'schedule.repeat': 'Wiederholen', + 'schedule.repeat_none': 'Keine Wiederholung', + 'schedule.repeat_daily': 'Täglich', + 'schedule.repeat_weekdays': 'Wochentags', + 'schedule.repeat_weekends': 'Wochenenden', + 'schedule.repeat_weekly': 'Wöchentlich', + 'schedule.priority': 'Priorität', + 'schedule.color': 'Farbe', + 'schedule.scheduled_label': 'Geplant', + 'schedule.tooltip_group_prefix': 'Gruppe: ', + 'schedule.tooltip_priority': 'Priorität: {n}', + 'schedule.day.sun': 'So', + 'schedule.day.mon': 'Mo', + 'schedule.day.tue': 'Di', + 'schedule.day.wed': 'Mi', + 'schedule.day.thu': 'Do', + 'schedule.day.fri': 'Fr', + 'schedule.day.sat': 'Sa', + 'schedule.hour_12am': '00 Uhr', + 'schedule.hour_am': ' Uhr', + 'schedule.hour_12pm': '12 Uhr', + 'schedule.hour_pm': ' Uhr', + 'schedule.toast.no_groups': 'Keine Gruppen verfügbar. Erstellen Sie zuerst eine.', + 'schedule.toast.saved': 'Zeitplan gespeichert', + + // Reports + 'report.title': 'Berichte', + 'report.subtitle': 'Wiedergabe-Analytik und Geräteverfügbarkeit', + 'report.help_tip': 'Wiedergabe-Analytik. Sehen Sie, was wann auf welchem Gerät gespielt wurde. Filtern Sie nach Zeitraum und Gerät. CSV-Export für Verifizierung.', + 'report.export_csv': 'CSV exportieren', + 'report.device': 'Gerät', + 'report.all_devices': 'Alle Geräte', + 'report.start_date': 'Startdatum', + 'report.end_date': 'Enddatum', + 'report.load_report': 'Bericht laden', + 'report.select_range': 'Wählen Sie einen Zeitraum und klicken Sie auf Bericht laden', + 'report.error': 'Fehler', + 'report.total_plays': 'Wiedergaben gesamt', + 'report.total_hours': 'Stunden gesamt', + 'report.unique_content': 'Einzigartiger Inhalt', + 'report.active_devices': 'Aktive Geräte', + 'report.avg_duration': 'Durchschn. Dauer', + 'report.plays_per_day': 'Wiedergaben pro Tag', + 'report.plays_by_hour': 'Wiedergaben pro Stunde', + 'report.top_content': 'Top-Inhalte', + 'report.by_device': 'Nach Gerät', + 'report.no_data': 'Keine Daten', + 'report.col.content': 'Inhalt', + 'report.col.device': 'Gerät', + 'report.col.plays': 'Wiedergaben', + 'report.col.total_hours': 'Stunden gesamt', + 'report.col.completion': 'Abschluss', + + // Kiosk + 'kiosk.title': 'Kioskseiten', + 'kiosk.subtitle': 'Erstellen Sie interaktive Touchscreen-Oberflächen', + 'kiosk.help_tip': 'Erstellen Sie interaktive Touchscreen-Oberflächen. Fügen Sie Buttons mit Symbolen und Aktionen hinzu. Inklusive Leerlaufbildschirm. Geräten als Widget zuweisen.', + 'kiosk.new_page': 'Neue Kioskseite', + 'kiosk.prompt_name': 'Name der Kioskseite:', + 'kiosk.empty_title': 'Noch keine Kioskseiten', + 'kiosk.empty_desc': 'Erstellen Sie eine interaktive Touchscreen-Oberfläche für Ihre Bildschirme.', + 'kiosk.label': 'Kioskseite', + 'kiosk.preview': 'Vorschau', + 'kiosk.confirm_delete': 'Kioskseite „{name}" löschen? Dies kann nicht rückgängig gemacht werden.', + 'kiosk.toast.deleted': 'Kioskseite gelöscht', + 'kiosk.toast.delete_failed': 'Löschen fehlgeschlagen', + 'kiosk.toast.saved': 'Kioskseite gespeichert', + 'kiosk.not_found': 'Seite nicht gefunden', + 'kiosk.back': 'Zurück zu Kioskseiten', + 'kiosk.page_settings': 'Seiteneinstellungen', + 'kiosk.title_label': 'Titel', + 'kiosk.subtitle_label': 'Untertitel', + 'kiosk.logo_url': 'Logo-URL', + 'kiosk.footer_text': 'Fußzeile', + 'kiosk.idle_title': 'Leerlaufbildschirm-Titel', + 'kiosk.idle_default': 'Zum Starten berühren', + 'kiosk.idle_timeout': 'Leerlauf-Timeout (Sekunden)', + 'kiosk.style': 'Stil', + 'kiosk.background': 'Hintergrund', + 'kiosk.text_color': 'Textfarbe', + 'kiosk.columns': 'Spalten', + 'kiosk.button_color': 'Button-Farbe', + 'kiosk.button_hover': 'Button-Hoverfarbe', + 'kiosk.buttons': 'Buttons', + 'kiosk.add_btn': '+ Hinzufügen', + 'kiosk.icon_placeholder': 'Emoji', + 'kiosk.label_placeholder': 'Beschriftung', + 'kiosk.sublabel_placeholder': 'Unterbeschriftung', + 'kiosk.action_none': 'Keine Aktion', + 'kiosk.action_url': 'URL öffnen', + 'kiosk.action_page': 'Zu Seite', + 'kiosk.url_placeholder': 'URL oder Seite', + 'kiosk.no_buttons': 'Noch keine Buttons', + 'kiosk.new_button': 'Neuer Button', }; diff --git a/frontend/js/i18n/en.js b/frontend/js/i18n/en.js index 36199ea..e76608e 100644 --- a/frontend/js/i18n/en.js +++ b/frontend/js/i18n/en.js @@ -804,4 +804,125 @@ export default { 'admin.toast.role_updated': 'Role updated', 'admin.toast.plan_updated': 'Plan updated', 'admin.toast.user_removed': 'User removed', + + // 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', }; diff --git a/frontend/js/i18n/es.js b/frontend/js/i18n/es.js index 8139577..a30ff19 100644 --- a/frontend/js/i18n/es.js +++ b/frontend/js/i18n/es.js @@ -767,4 +767,125 @@ export default { 'admin.toast.role_updated': 'Rol actualizado', 'admin.toast.plan_updated': 'Plan actualizado', 'admin.toast.user_removed': 'Usuario eliminado', + + // Schedule + 'schedule.title': 'Horario', + 'schedule.subtitle': 'Calendario de programación de contenido', + 'schedule.help_tip': 'Calendario semanal visual para programación. Haz clic en Agregar horario para crear franjas. Configura recurrencia para repetir contenido. La prioridad mayor anula la menor. Los horarios de dispositivo anulan los de grupo.', + 'schedule.prev_week': '< Anterior', + 'schedule.next_week': 'Siguiente >', + 'schedule.add_schedule': 'Agregar horario', + 'schedule.edit_schedule': 'Editar horario', + 'schedule.apply_to': 'Aplicar a', + 'schedule.target_device': 'Dispositivo', + 'schedule.target_group': 'Grupo', + 'schedule.group_devices_count': '{n} dispositivos', + 'schedule.no_groups_msg': 'Aún no hay grupos creados. Créalos en la página Pantallas.', + 'schedule.zone_note': 'Nota: Los horarios por zona dependen del diseño. Asegúrate de que todos los dispositivos del grupo usan el mismo diseño.', + 'schedule.playlist_override': 'Sobrescribir lista', + 'schedule.no_playlist_override': '— Sin sobrescribir lista —', + 'schedule.draft_suffix': '(borrador)', + 'schedule.layout_override': 'Sobrescribir diseño', + 'schedule.no_layout_override': '— Sin sobrescribir diseño —', + 'schedule.content_label': 'Contenido', + 'schedule.content_hint': '(elemento único, opcional)', + 'schedule.content_none': '— Ninguno —', + 'schedule.title_label': 'Título (opcional)', + 'schedule.title_placeholder': 'p. ej., Lista mañana', + 'schedule.start_time': 'Hora de inicio', + 'schedule.end_time': 'Hora de fin', + 'schedule.repeat': 'Repetir', + 'schedule.repeat_none': 'No repetir', + 'schedule.repeat_daily': 'Diario', + 'schedule.repeat_weekdays': 'Días laborables', + 'schedule.repeat_weekends': 'Fines de semana', + 'schedule.repeat_weekly': 'Semanal', + 'schedule.priority': 'Prioridad', + 'schedule.color': 'Color', + 'schedule.scheduled_label': 'Programado', + 'schedule.tooltip_group_prefix': 'Grupo: ', + 'schedule.tooltip_priority': 'Prioridad: {n}', + 'schedule.day.sun': 'Dom', + 'schedule.day.mon': 'Lun', + 'schedule.day.tue': 'Mar', + 'schedule.day.wed': 'Mié', + 'schedule.day.thu': 'Jue', + 'schedule.day.fri': 'Vie', + 'schedule.day.sat': 'Sáb', + 'schedule.hour_12am': '12am', + 'schedule.hour_am': 'am', + 'schedule.hour_12pm': '12pm', + 'schedule.hour_pm': 'pm', + 'schedule.toast.no_groups': 'No hay grupos disponibles. Crea uno primero.', + 'schedule.toast.saved': 'Horario guardado', + + // Reports + 'report.title': 'Informes', + 'report.subtitle': 'Análisis de reproducción y disponibilidad de dispositivos', + 'report.help_tip': 'Análisis de reproducción. Ve qué se reprodujo, cuándo y en qué dispositivo. Filtra por rango de fechas y dispositivo. Exporta a CSV para verificación.', + 'report.export_csv': 'Exportar CSV', + 'report.device': 'Dispositivo', + 'report.all_devices': 'Todos los dispositivos', + 'report.start_date': 'Fecha de inicio', + 'report.end_date': 'Fecha de fin', + 'report.load_report': 'Cargar informe', + 'report.select_range': 'Selecciona un rango de fechas y haz clic en Cargar informe', + 'report.error': 'Error', + 'report.total_plays': 'Reproducciones totales', + 'report.total_hours': 'Horas totales', + 'report.unique_content': 'Contenido único', + 'report.active_devices': 'Dispositivos activos', + 'report.avg_duration': 'Duración media', + 'report.plays_per_day': 'Reproducciones por día', + 'report.plays_by_hour': 'Reproducciones por hora', + 'report.top_content': 'Contenido más reproducido', + 'report.by_device': 'Por dispositivo', + 'report.no_data': 'Sin datos', + 'report.col.content': 'Contenido', + 'report.col.device': 'Dispositivo', + 'report.col.plays': 'Reproducciones', + 'report.col.total_hours': 'Horas totales', + 'report.col.completion': 'Finalización', + + // Kiosk + 'kiosk.title': 'Páginas de kiosco', + 'kiosk.subtitle': 'Crea interfaces táctiles interactivas', + 'kiosk.help_tip': 'Crea interfaces táctiles interactivas. Agrega botones con iconos y acciones. Incluye pantalla inactiva tras inactividad. Asígnalas a dispositivos como widget.', + 'kiosk.new_page': 'Nueva página de kiosco', + 'kiosk.prompt_name': 'Nombre de la página de kiosco:', + 'kiosk.empty_title': 'Aún no hay páginas', + 'kiosk.empty_desc': 'Crea una interfaz táctil interactiva para tus pantallas.', + 'kiosk.label': 'Página de kiosco', + 'kiosk.preview': 'Previsualizar', + 'kiosk.confirm_delete': '¿Eliminar la página "{name}"? Esto no se puede deshacer.', + 'kiosk.toast.deleted': 'Página eliminada', + 'kiosk.toast.delete_failed': 'Error al eliminar', + 'kiosk.toast.saved': 'Página guardada', + 'kiosk.not_found': 'Página no encontrada', + 'kiosk.back': 'Volver a páginas', + 'kiosk.page_settings': 'Ajustes de la página', + 'kiosk.title_label': 'Título', + 'kiosk.subtitle_label': 'Subtítulo', + 'kiosk.logo_url': 'URL del logotipo', + 'kiosk.footer_text': 'Texto del pie', + 'kiosk.idle_title': 'Título pantalla inactiva', + 'kiosk.idle_default': 'Toca para comenzar', + 'kiosk.idle_timeout': 'Tiempo de inactividad (segundos)', + 'kiosk.style': 'Estilo', + 'kiosk.background': 'Fondo', + 'kiosk.text_color': 'Color del texto', + 'kiosk.columns': 'Columnas', + 'kiosk.button_color': 'Color del botón', + 'kiosk.button_hover': 'Color hover del botón', + 'kiosk.buttons': 'Botones', + 'kiosk.add_btn': '+ Agregar', + 'kiosk.icon_placeholder': 'Emoji', + 'kiosk.label_placeholder': 'Etiqueta', + 'kiosk.sublabel_placeholder': 'Sub-etiqueta', + 'kiosk.action_none': 'Sin acción', + 'kiosk.action_url': 'Abrir URL', + 'kiosk.action_page': 'Ir a página', + 'kiosk.url_placeholder': 'URL o página', + 'kiosk.no_buttons': 'Aún no hay botones', + 'kiosk.new_button': 'Nuevo botón', }; diff --git a/frontend/js/i18n/fr.js b/frontend/js/i18n/fr.js index 4a51975..2f1f369 100644 --- a/frontend/js/i18n/fr.js +++ b/frontend/js/i18n/fr.js @@ -768,4 +768,125 @@ export default { 'admin.toast.role_updated': 'Rôle mis à jour', 'admin.toast.plan_updated': 'Plan mis à jour', 'admin.toast.user_removed': 'Utilisateur retiré', + + // Schedule + 'schedule.title': 'Calendrier', + 'schedule.subtitle': 'Calendrier de programmation du contenu', + 'schedule.help_tip': 'Calendrier hebdomadaire visuel pour la programmation. Cliquez sur Ajouter une plage pour créer des créneaux. La récurrence permet de répéter du contenu. La priorité plus haute prime. Les plages au niveau appareil priment sur celles de groupe.', + 'schedule.prev_week': '< Préc', + 'schedule.next_week': 'Suiv >', + 'schedule.add_schedule': 'Ajouter une plage', + 'schedule.edit_schedule': 'Modifier la plage', + 'schedule.apply_to': 'Appliquer à', + 'schedule.target_device': 'Appareil', + 'schedule.target_group': 'Groupe', + 'schedule.group_devices_count': '{n} appareils', + 'schedule.no_groups_msg': 'Aucun groupe créé. Créez-en sur la page Écrans.', + 'schedule.zone_note': 'Note : les plages par zone dépendent de la mise en page. Assurez-vous que tous les appareils du groupe utilisent la même.', + 'schedule.playlist_override': 'Liste prioritaire', + 'schedule.no_playlist_override': '— Pas de liste prioritaire —', + 'schedule.draft_suffix': '(brouillon)', + 'schedule.layout_override': 'Mise en page prioritaire', + 'schedule.no_layout_override': '— Pas de mise en page prioritaire —', + 'schedule.content_label': 'Contenu', + 'schedule.content_hint': '(élément unique, facultatif)', + 'schedule.content_none': '— Aucun —', + 'schedule.title_label': 'Titre (facultatif)', + 'schedule.title_placeholder': 'ex. Liste matin', + 'schedule.start_time': 'Début', + 'schedule.end_time': 'Fin', + 'schedule.repeat': 'Répéter', + 'schedule.repeat_none': 'Ne pas répéter', + 'schedule.repeat_daily': 'Quotidien', + 'schedule.repeat_weekdays': 'Jours ouvrables', + 'schedule.repeat_weekends': 'Week-ends', + 'schedule.repeat_weekly': 'Hebdomadaire', + 'schedule.priority': 'Priorité', + 'schedule.color': 'Couleur', + 'schedule.scheduled_label': 'Programmé', + 'schedule.tooltip_group_prefix': 'Groupe : ', + 'schedule.tooltip_priority': 'Priorité : {n}', + 'schedule.day.sun': 'Dim', + 'schedule.day.mon': 'Lun', + 'schedule.day.tue': 'Mar', + 'schedule.day.wed': 'Mer', + 'schedule.day.thu': 'Jeu', + 'schedule.day.fri': 'Ven', + 'schedule.day.sat': 'Sam', + 'schedule.hour_12am': '00h', + 'schedule.hour_am': 'h', + 'schedule.hour_12pm': '12h', + 'schedule.hour_pm': 'h', + 'schedule.toast.no_groups': 'Aucun groupe disponible. Créez-en un d\'abord.', + 'schedule.toast.saved': 'Plage enregistrée', + + // Reports + 'report.title': 'Rapports', + 'report.subtitle': 'Analyses de lecture et disponibilité des appareils', + 'report.help_tip': 'Analyses de lecture. Voyez ce qui a été lu, quand et sur quel appareil. Filtrez par période et appareil. Export CSV pour vérification.', + 'report.export_csv': 'Exporter CSV', + 'report.device': 'Appareil', + 'report.all_devices': 'Tous les appareils', + 'report.start_date': 'Date de début', + 'report.end_date': 'Date de fin', + 'report.load_report': 'Charger le rapport', + 'report.select_range': 'Sélectionnez une période et cliquez sur Charger le rapport', + 'report.error': 'Erreur', + 'report.total_plays': 'Lectures totales', + 'report.total_hours': 'Heures totales', + 'report.unique_content': 'Contenu unique', + 'report.active_devices': 'Appareils actifs', + 'report.avg_duration': 'Durée moyenne', + 'report.plays_per_day': 'Lectures par jour', + 'report.plays_by_hour': 'Lectures par heure', + 'report.top_content': 'Contenu le plus lu', + 'report.by_device': 'Par appareil', + 'report.no_data': 'Aucune donnée', + 'report.col.content': 'Contenu', + 'report.col.device': 'Appareil', + 'report.col.plays': 'Lectures', + 'report.col.total_hours': 'Heures totales', + 'report.col.completion': 'Achèvement', + + // Kiosk + 'kiosk.title': 'Pages kiosque', + 'kiosk.subtitle': 'Créez des interfaces tactiles interactives', + 'kiosk.help_tip': 'Créez des interfaces tactiles interactives. Ajoutez des boutons avec icônes et actions. Inclut un écran d\'attente après inactivité. Attribuez aux appareils comme widget.', + 'kiosk.new_page': 'Nouvelle page kiosque', + 'kiosk.prompt_name': 'Nom de la page kiosque :', + 'kiosk.empty_title': 'Aucune page kiosque', + 'kiosk.empty_desc': 'Créez une interface tactile interactive pour vos écrans.', + 'kiosk.label': 'Page kiosque', + 'kiosk.preview': 'Aperçu', + 'kiosk.confirm_delete': 'Supprimer la page « {name} » ? Cette action est irréversible.', + 'kiosk.toast.deleted': 'Page supprimée', + 'kiosk.toast.delete_failed': 'Échec de la suppression', + 'kiosk.toast.saved': 'Page enregistrée', + 'kiosk.not_found': 'Page introuvable', + 'kiosk.back': 'Retour aux pages', + 'kiosk.page_settings': 'Paramètres de la page', + 'kiosk.title_label': 'Titre', + 'kiosk.subtitle_label': 'Sous-titre', + 'kiosk.logo_url': 'URL du logo', + 'kiosk.footer_text': 'Texte de pied', + 'kiosk.idle_title': 'Titre écran d\'attente', + 'kiosk.idle_default': 'Touchez pour commencer', + 'kiosk.idle_timeout': 'Délai d\'inactivité (secondes)', + 'kiosk.style': 'Style', + 'kiosk.background': 'Fond', + 'kiosk.text_color': 'Couleur du texte', + 'kiosk.columns': 'Colonnes', + 'kiosk.button_color': 'Couleur des boutons', + 'kiosk.button_hover': 'Couleur survol', + 'kiosk.buttons': 'Boutons', + 'kiosk.add_btn': '+ Ajouter', + 'kiosk.icon_placeholder': 'Emoji', + 'kiosk.label_placeholder': 'Étiquette', + 'kiosk.sublabel_placeholder': 'Sous-étiquette', + 'kiosk.action_none': 'Aucune action', + 'kiosk.action_url': 'Ouvrir l\'URL', + 'kiosk.action_page': 'Aller à la page', + 'kiosk.url_placeholder': 'URL ou page', + 'kiosk.no_buttons': 'Aucun bouton', + 'kiosk.new_button': 'Nouveau bouton', }; diff --git a/frontend/js/i18n/pt.js b/frontend/js/i18n/pt.js index 4be0a66..92a498a 100644 --- a/frontend/js/i18n/pt.js +++ b/frontend/js/i18n/pt.js @@ -768,4 +768,125 @@ export default { 'admin.toast.role_updated': 'Função atualizada', 'admin.toast.plan_updated': 'Plano atualizado', 'admin.toast.user_removed': 'Usuário removido', + + // Schedule + 'schedule.title': 'Agenda', + 'schedule.subtitle': 'Calendário de programação de conteúdo', + 'schedule.help_tip': 'Calendário semanal visual para agendamento. Clique em Adicionar para criar slots. Defina recorrência para conteúdo recorrente. Prioridade maior sobrepõe menor. Agendas de dispositivo sobrepõem as de grupo.', + 'schedule.prev_week': '< Anterior', + 'schedule.next_week': 'Próxima >', + 'schedule.add_schedule': 'Adicionar agenda', + 'schedule.edit_schedule': 'Editar agenda', + 'schedule.apply_to': 'Aplicar a', + 'schedule.target_device': 'Dispositivo', + 'schedule.target_group': 'Grupo', + 'schedule.group_devices_count': '{n} dispositivos', + 'schedule.no_groups_msg': 'Sem grupos criados ainda. Crie na página Telas.', + 'schedule.zone_note': 'Nota: Agendas por zona dependem do layout. Garanta que todos dispositivos do grupo usem o mesmo layout.', + 'schedule.playlist_override': 'Sobrepor playlist', + 'schedule.no_playlist_override': '— Sem sobreposição —', + 'schedule.draft_suffix': '(rascunho)', + 'schedule.layout_override': 'Sobrepor layout', + 'schedule.no_layout_override': '— Sem sobreposição —', + 'schedule.content_label': 'Conteúdo', + 'schedule.content_hint': '(item único, opcional)', + 'schedule.content_none': '— Nenhum —', + 'schedule.title_label': 'Título (opcional)', + 'schedule.title_placeholder': 'ex. Playlist matinal', + 'schedule.start_time': 'Início', + 'schedule.end_time': 'Fim', + 'schedule.repeat': 'Repetir', + 'schedule.repeat_none': 'Sem repetição', + 'schedule.repeat_daily': 'Diário', + 'schedule.repeat_weekdays': 'Dias úteis', + 'schedule.repeat_weekends': 'Fins de semana', + 'schedule.repeat_weekly': 'Semanal', + 'schedule.priority': 'Prioridade', + 'schedule.color': 'Cor', + 'schedule.scheduled_label': 'Agendado', + 'schedule.tooltip_group_prefix': 'Grupo: ', + 'schedule.tooltip_priority': 'Prioridade: {n}', + 'schedule.day.sun': 'Dom', + 'schedule.day.mon': 'Seg', + 'schedule.day.tue': 'Ter', + 'schedule.day.wed': 'Qua', + 'schedule.day.thu': 'Qui', + 'schedule.day.fri': 'Sex', + 'schedule.day.sat': 'Sáb', + 'schedule.hour_12am': '00h', + 'schedule.hour_am': 'h', + 'schedule.hour_12pm': '12h', + 'schedule.hour_pm': 'h', + 'schedule.toast.no_groups': 'Nenhum grupo disponível. Crie um primeiro.', + 'schedule.toast.saved': 'Agenda salva', + + // Reports + 'report.title': 'Relatórios', + 'report.subtitle': 'Análise de exibição e disponibilidade dos dispositivos', + 'report.help_tip': 'Análise de exibição. Veja o que foi exibido, quando e em qual dispositivo. Filtre por período e dispositivo. Exporte CSV para verificação.', + 'report.export_csv': 'Exportar CSV', + 'report.device': 'Dispositivo', + 'report.all_devices': 'Todos os dispositivos', + 'report.start_date': 'Data de início', + 'report.end_date': 'Data de fim', + 'report.load_report': 'Carregar relatório', + 'report.select_range': 'Selecione um período e clique em Carregar relatório', + 'report.error': 'Erro', + 'report.total_plays': 'Exibições totais', + 'report.total_hours': 'Horas totais', + 'report.unique_content': 'Conteúdo único', + 'report.active_devices': 'Dispositivos ativos', + 'report.avg_duration': 'Duração média', + 'report.plays_per_day': 'Exibições por dia', + 'report.plays_by_hour': 'Exibições por hora', + 'report.top_content': 'Conteúdo mais exibido', + 'report.by_device': 'Por dispositivo', + 'report.no_data': 'Sem dados', + 'report.col.content': 'Conteúdo', + 'report.col.device': 'Dispositivo', + 'report.col.plays': 'Exibições', + 'report.col.total_hours': 'Horas totais', + 'report.col.completion': 'Conclusão', + + // Kiosk + 'kiosk.title': 'Páginas de quiosque', + 'kiosk.subtitle': 'Crie interfaces de toque interativas', + 'kiosk.help_tip': 'Crie interfaces de toque interativas. Adicione botões com ícones e ações. Inclui tela ociosa após inatividade. Atribua a dispositivos como widget.', + 'kiosk.new_page': 'Nova página de quiosque', + 'kiosk.prompt_name': 'Nome da página:', + 'kiosk.empty_title': 'Sem páginas ainda', + 'kiosk.empty_desc': 'Crie uma interface interativa para suas telas.', + 'kiosk.label': 'Página de quiosque', + 'kiosk.preview': 'Pré-visualizar', + 'kiosk.confirm_delete': 'Excluir a página "{name}"? Isso não pode ser desfeito.', + 'kiosk.toast.deleted': 'Página excluída', + 'kiosk.toast.delete_failed': 'Falha ao excluir', + 'kiosk.toast.saved': 'Página salva', + 'kiosk.not_found': 'Página não encontrada', + 'kiosk.back': 'Voltar para páginas', + 'kiosk.page_settings': 'Configurações da página', + 'kiosk.title_label': 'Título', + 'kiosk.subtitle_label': 'Subtítulo', + 'kiosk.logo_url': 'URL do logotipo', + 'kiosk.footer_text': 'Texto do rodapé', + 'kiosk.idle_title': 'Título da tela ociosa', + 'kiosk.idle_default': 'Toque para começar', + 'kiosk.idle_timeout': 'Tempo de inatividade (segundos)', + 'kiosk.style': 'Estilo', + 'kiosk.background': 'Fundo', + 'kiosk.text_color': 'Cor do texto', + 'kiosk.columns': 'Colunas', + 'kiosk.button_color': 'Cor do botão', + 'kiosk.button_hover': 'Cor do hover', + 'kiosk.buttons': 'Botões', + 'kiosk.add_btn': '+ Adicionar', + 'kiosk.icon_placeholder': 'Emoji', + 'kiosk.label_placeholder': 'Rótulo', + 'kiosk.sublabel_placeholder': 'Sub-rótulo', + 'kiosk.action_none': 'Sem ação', + 'kiosk.action_url': 'Abrir URL', + 'kiosk.action_page': 'Ir para página', + 'kiosk.url_placeholder': 'URL ou página', + 'kiosk.no_buttons': 'Sem botões ainda', + 'kiosk.new_button': 'Novo botão', }; diff --git a/frontend/js/views/kiosk.js b/frontend/js/views/kiosk.js index b70cac5..6463c99 100644 --- a/frontend/js/views/kiosk.js +++ b/frontend/js/views/kiosk.js @@ -1,4 +1,5 @@ import { showToast } from '../components/toast.js'; +import { t } from '../i18n.js'; const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(r => r.json()); @@ -14,17 +15,17 @@ export async function render(container) { async function renderList(container) { container.innerHTML = `
`; document.getElementById('newKioskBtn').onclick = async () => { - const name = prompt('Kiosk page name:'); + const name = prompt(t('kiosk.prompt_name')); if (!name) return; const page = await API('/kiosk', { method: 'POST', body: JSON.stringify({ name }) }); window.location.hash = `#/kiosk/${page.id}`; @@ -34,7 +35,7 @@ async function renderList(container) { const pages = await API('/kiosk'); const grid = document.getElementById('kioskGrid'); if (!pages.length) { - grid.innerHTML = '

No kiosk pages yet

Create an interactive touchscreen interface for your displays.

'; + grid.innerHTML = `

${t('kiosk.empty_title')}

${t('kiosk.empty_desc')}

`; return; } grid.innerHTML = pages.map(p => ` @@ -44,27 +45,26 @@ async function renderList(container) {
${p.name}
-
Kiosk Page
+
${t('kiosk.label')}
- Preview - + ${t('kiosk.preview')} +
`).join(''); - // Delete handler grid.querySelectorAll('[data-delete-kiosk]').forEach(btn => { btn.onclick = async (e) => { e.stopPropagation(); const name = btn.dataset.kioskName; - if (!confirm(`Delete kiosk page "${name}"? This cannot be undone.`)) return; + if (!confirm(t('kiosk.confirm_delete', { name }))) return; try { await API(`/kiosk/${btn.dataset.deleteKiosk}`, { method: 'DELETE' }); - showToast('Kiosk page deleted'); + showToast(t('kiosk.toast.deleted')); renderList(container); } catch (err) { - showToast(err.message || 'Failed to delete', 'error'); + showToast(err.message || t('kiosk.toast.delete_failed'), 'error'); } }; }); @@ -73,7 +73,7 @@ async function renderList(container) { async function renderEditor(container, pageId) { let page; - try { page = await API(`/kiosk/${pageId}`); } catch { container.innerHTML = '

Page not found

'; return; } + try { page = await API(`/kiosk/${pageId}`); } catch { container.innerHTML = `

${t('kiosk.not_found')}

`; return; } let config = JSON.parse(page.config || '{}'); if (!config.buttons) config.buttons = []; @@ -82,49 +82,47 @@ async function renderEditor(container, pageId) { container.innerHTML = ` - Back to Kiosk Pages + ${t('kiosk.back')}
-
-
-

Page Settings

-
-
-
-
-
-
+

${t('kiosk.page_settings')}

+
+
+
+
+
+
-

Style

-
-
-
+
+
-
-
+
+
-

Buttons

- +

${t('kiosk.buttons')}

+
@@ -137,23 +135,22 @@ async function renderEditor(container, pageId) { list.innerHTML = config.buttons.map((btn, i) => `
- - + +
- +
- +
- +
- `).join('') || '

No buttons yet

'; + `).join('') || `

${t('kiosk.no_buttons')}

`; - // Bind inputs list.querySelectorAll('[data-btn]').forEach(input => { input.oninput = () => { const idx = parseInt(input.dataset.btn); @@ -168,7 +165,7 @@ async function renderEditor(container, pageId) { } document.getElementById('addBtnBtn').onclick = () => { - config.buttons.push({ label: 'New Button', sublabel: '', icon: '⭐', action: '', url: '' }); + config.buttons.push({ label: t('kiosk.new_button'), sublabel: '', icon: '⭐', action: '', url: '' }); renderButtons(); }; @@ -190,7 +187,7 @@ async function renderEditor(container, pageId) { try { await API(`/kiosk/${pageId}`, { method: 'PUT', body: JSON.stringify({ config }) }); - showToast('Kiosk page saved', 'success'); + showToast(t('kiosk.toast.saved'), 'success'); document.getElementById('kioskPreview').src = `/api/kiosk/${pageId}/render?t=${Date.now()}`; } catch (err) { showToast(err.message, 'error'); } }; diff --git a/frontend/js/views/reports.js b/frontend/js/views/reports.js index f295b53..d5ac3cd 100644 --- a/frontend/js/views/reports.js +++ b/frontend/js/views/reports.js @@ -1,6 +1,7 @@ import { api } from '../api.js'; import { showToast } from '../components/toast.js'; import { esc } from '../utils.js'; +import { t } from '../i18n.js'; const API = (url, opts = {}) => fetch('/api' + url, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(r => r.json()); @@ -12,36 +13,36 @@ export async function render(container) { container.innerHTML = `
-
+
-
+
-
+
- +
-

Select a date range and click Load Report

+

${t('report.select_range')}

`; document.getElementById('loadReportBtn').onclick = loadReport; - loadReport(); // Auto-load on page render + loadReport(); document.getElementById('exportBtn').onclick = () => { const deviceId = document.getElementById('reportDevice').value; const start = document.getElementById('reportStart').value; @@ -56,84 +57,79 @@ export async function render(container) { const end = document.getElementById('reportEnd').value; const content = document.getElementById('reportContent'); - content.innerHTML = '

Loading...

'; + content.innerHTML = `

${t('common.loading')}

`; try { const summary = await API(`/reports/summary?device_id=${deviceId}&start=${start}&end=${end}`); content.innerHTML = ` -
-
Total Plays
+
${t('report.total_plays')}
${summary.overall.total_plays.toLocaleString()}
-
Total Hours
+
${t('report.total_hours')}
${summary.overall.total_hours}
-
Unique Content
+
${t('report.unique_content')}
${summary.overall.unique_content}
-
Active Devices
+
${t('report.active_devices')}
${summary.overall.unique_devices}
-
Avg Duration
+
${t('report.avg_duration')}
${formatDuration(summary.overall.avg_duration_sec)}
-
-

Plays per Day

+

${t('report.plays_per_day')}

-
-

Plays by Hour

+

${t('report.plays_by_hour')}

-
-

Top Content

+

${t('report.top_content')}

- - - - + + + + ${summary.by_content.map(c => ` - + - `).join('') || ''} + `).join('') || ``}
ContentPlaysTotal HoursCompletion${t('report.col.content')}${t('report.col.plays')}${t('report.col.total_hours')}${t('report.col.completion')}
${c.content_name || 'Unknown'}${c.content_name || t('common.unknown')} ${c.plays} ${(c.total_seconds / 3600).toFixed(1)} ${c.plays > 0 ? Math.round((c.completed_plays / c.plays) * 100) : 0}%
No data
${t('report.no_data')}
-
-

By Device

+

${t('report.by_device')}

- - - + + + ${summary.by_device.map(d => ` @@ -142,20 +138,18 @@ export async function render(container) { - `).join('') || ''} + `).join('') || ``}
DevicePlaysTotal Hours${t('report.col.device')}${t('report.col.plays')}${t('report.col.total_hours')}
${d.plays} ${(d.total_seconds / 3600).toFixed(1)}
No data
${t('report.no_data')}
`; - // Render daily chart renderBarChart('dailyChart', summary.by_day.map(d => ({ - label: new Date(d.day).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), + label: new Date(d.day).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }), value: d.plays }))); - // Render hourly chart const hourData = Array.from({ length: 24 }, (_, i) => { const found = summary.by_hour.find(h => h.hour === i); return { label: i === 0 ? '12a' : i < 12 ? i + 'a' : i === 12 ? '12p' : (i - 12) + 'p', value: found?.plays || 0 }; @@ -163,7 +157,7 @@ export async function render(container) { renderBarChart('hourlyChart', hourData); } catch (err) { - content.innerHTML = `

Error

${esc(err.message)}

`; + content.innerHTML = `

${t('report.error')}

${esc(err.message)}

`; } } } diff --git a/frontend/js/views/schedule.js b/frontend/js/views/schedule.js index aa4935c..e4f704a 100644 --- a/frontend/js/views/schedule.js +++ b/frontend/js/views/schedule.js @@ -1,10 +1,10 @@ import { api } from '../api.js'; import { showToast } from '../components/toast.js'; +import { t } from '../i18n.js'; const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(r => r.json()); const HOURS = Array.from({ length: 24 }, (_, i) => i); -const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; function esc(str) { const d = document.createElement('div'); d.textContent = str; return d.innerHTML; } @@ -17,95 +17,99 @@ export async function render(container) { API('/layouts'), ]); const layouts = (Array.isArray(layoutsRaw) ? layoutsRaw : []).filter(l => !l.is_template); - const selectedDevice = devices[0]?.id || ''; const today = new Date(); const weekStart = new Date(today); weekStart.setDate(today.getDate() - today.getDay()); weekStart.setHours(0, 0, 0, 0); + const DAYS = [ + t('schedule.day.sun'), t('schedule.day.mon'), t('schedule.day.tue'), + t('schedule.day.wed'), t('schedule.day.thu'), t('schedule.day.fri'), + t('schedule.day.sat'), + ]; + container.innerHTML = `
- + - - + +
-