diff --git a/package.json b/package.json index 9fab1c55..866c3dd2 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,13 @@ "fortawesome": "^0.0.1-security", "google-auth-library": "^9.14.1", "googleapis": "^144.0.0", + + "i18next": "^24.0.2", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^3.0.1", + "idcac-playwright": "^0.1.3", + "ioredis": "^5.4.1", "joi": "^17.6.0", "jsonwebtoken": "^9.0.2", @@ -57,6 +63,7 @@ "react": "^18.0.0", "react-dom": "^18.0.0", "react-highlight": "0.15.0", + "react-i18next": "^15.1.3", "react-router-dom": "^6.26.1", "react-simple-code-editor": "^0.11.2", "react-transition-group": "^4.4.2", diff --git a/public/locales/de.json b/public/locales/de.json new file mode 100644 index 00000000..28855c78 --- /dev/null +++ b/public/locales/de.json @@ -0,0 +1,486 @@ +{ + "login": { + "title": "Willkommen zurück!", + "email": "E-Mail", + "password": "Passwort", + "button": "Einloggen", + "loading": "Lädt", + "register_prompt": "Noch keinen Account?", + "register_link": "Registrieren", + "welcome_notification": "Willkommen bei Maxun!", + "error_notification": "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut." + }, + "register": { + "title": "Konto registrieren", + "email": "E-Mail", + "password": "Passwort", + "button": "Registrieren", + "loading": "Lädt", + "register_prompt": "Bereits ein Konto?", + "login_link": "Einloggen", + "welcome_notification": "Willkommen bei Maxun!", + "error_notification": "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut." + }, + "recordingtable": { + "run": "Ausführen", + "name": "Name", + "schedule": "Zeitplan", + "integrate": "Integrieren", + "settings": "Einstellungen", + "options": "Optionen", + "heading": "Meine Roboter", + "new": "Roboter erstellen", + "modal": { + "title": "Geben Sie die URL ein", + "label": "URL", + "button": "Aufnahme starten" + }, + "edit": "Bearbeiten", + "delete": "Löschen", + "duplicate": "Duplizieren", + "notifications": { + "delete_warning": "Roboter kann nicht gelöscht werden, da zugehörige Ausführungen vorhanden sind", + "delete_success": "Roboter erfolgreich gelöscht" + } + }, + "mainmenu": { + "recordings": "Roboter", + "runs": "Ausführungen", + "proxy": "Proxy", + "apikey": "API-Schlüssel", + "feedback": "Maxun Cloud beitreten", + "apidocs": "Website zu API" + }, + "runstable": { + "runs": "Alle Ausführungen", + "runStatus": "Status", + "runName": "Name", + "startedAt": "Gestartet am", + "finishedAt": "Beendet am", + "delete": "Löschen", + "settings": "Einstellungen", + "search": "Ausführungen suchen...", + "notifications": { + "no_runs": "Keine Ausführungen gefunden. Bitte versuchen Sie es erneut.", + "delete_success": "Ausführung erfolgreich gelöscht" + } + }, + "proxy": { + "title": "Proxy-Konfiguration", + "tab_standard": "Standard-Proxy", + "tab_rotation": "Automatische Proxy-Rotation", + "server_url": "Proxy-Server-URL", + "server_url_helper": "Proxy für alle Roboter. HTTP- und SOCKS-Proxys werden unterstützt. Beispiel http://myproxy.com:3128 oder socks5://myproxy.com:3128. Kurzform myproxy.com:3128 wird als HTTP-Proxy behandelt.", + "requires_auth": "Authentifizierung erforderlich?", + "username": "Benutzername", + "password": "Passwort", + "add_proxy": "Proxy hinzufügen", + "test_proxy": "Proxy testen", + "remove_proxy": "Proxy entfernen", + "table": { + "proxy_url": "Proxy-URL", + "requires_auth": "Authentifizierung erforderlich" + }, + "coming_soon": "Demnächst verfügbar - In Open Source (Basis-Rotation) & Cloud (Erweiterte Rotation). Wenn Sie die Infrastruktur nicht selbst verwalten möchten, tragen Sie sich in unsere Cloud-Warteliste ein.", + "join_waitlist": "Maxun Cloud Warteliste beitreten", + "alert": { + "title": "Wenn Ihr Proxy einen Benutzernamen und ein Passwort erfordert, geben Sie diese immer separat von der Proxy-URL an.", + "right_way": "Der richtige Weg", + "wrong_way": "Der falsche Weg", + "proxy_url": "Proxy-URL:", + "username": "Benutzername:", + "password": "Passwort:" + }, + "notifications": { + "config_success": "Proxy-Konfiguration erfolgreich übermittelt", + "config_error": "Fehler beim Übermitteln der Proxy-Konfiguration. Bitte erneut versuchen.", + "test_success": "Proxy-Konfiguration funktioniert", + "test_error": "Fehler beim Testen der Proxy-Konfiguration. Bitte erneut versuchen.", + "fetch_success": "Proxy-Konfiguration erfolgreich abgerufen", + "remove_success": "Proxy-Konfiguration erfolgreich entfernt", + "remove_error": "Fehler beim Entfernen der Proxy-Konfiguration. Bitte erneut versuchen." + } + }, + "apikey": { + "title": "API-Schlüssel verwalten", + "default_name": "Maxun API-Schlüssel", + "table": { + "name": "API-Schlüssel Name", + "key": "API-Schlüssel", + "actions": "Aktionen" + }, + "actions": { + "copy": "Kopieren", + "show": "Anzeigen", + "hide": "Ausblenden", + "delete": "Löschen" + }, + "no_key_message": "Sie haben noch keinen API-Schlüssel generiert.", + "generate_button": "API-Schlüssel generieren", + "notifications": { + "fetch_error": "API-Schlüssel konnte nicht abgerufen werden - ${error}", + "generate_success": "API-Schlüssel erfolgreich generiert", + "generate_error": "API-Schlüssel konnte nicht generiert werden - ${error}", + "delete_success": "API-Schlüssel erfolgreich gelöscht", + "delete_error": "API-Schlüssel konnte nicht gelöscht werden - ${error}", + "copy_success": "API-Schlüssel erfolgreich kopiert" + } + }, + "action_description": { + "text": { + "title": "Text erfassen", + "description": "Fahren Sie über die Texte, die Sie extrahieren möchten, und klicken Sie, um sie auszuwählen" + }, + "screenshot": { + "title": "Screenshot erfassen", + "description": "Erfassen Sie einen Teil- oder Vollbildschirmfoto der aktuellen Seite." + }, + "list": { + "title": "Liste erfassen", + "description": "Fahren Sie über die Liste, die Sie extrahieren möchten. Nach der Auswahl können Sie über alle Texte in der ausgewählten Liste fahren. Klicken Sie zum Auswählen." + }, + "default": { + "title": "Welche Daten möchten Sie extrahieren?", + "description": "Ein Roboter ist darauf ausgelegt, eine Aktion nach der anderen auszuführen. Sie können eine der folgenden Optionen wählen." + }, + "list_stages": { + "initial": "Wählen Sie die Liste aus, die Sie extrahieren möchten, zusammen mit den darin enthaltenen Texten", + "pagination": "Wählen Sie aus, wie der Roboter den Rest der Liste erfassen kann", + "limit": "Wählen Sie die Anzahl der zu extrahierenden Elemente", + "complete": "Erfassung ist abgeschlossen" + } + }, + "right_panel": { + "buttons": { + "capture_list": "Liste erfassen", + "capture_text": "Text erfassen", + "capture_screenshot": "Screenshot erfassen", + "confirm": "Bestätigen", + "discard": "Verwerfen", + "confirm_capture": "Erfassung bestätigen", + "confirm_pagination": "Paginierung bestätigen", + "confirm_limit": "Limit bestätigen", + "finish_capture": "Erfassung abschließen", + "finish": "Fertig", + "cancel": "Abbrechen" + }, + "screenshot": { + "capture_fullpage": "Vollständige Seite erfassen", + "capture_visible": "Sichtbaren Bereich erfassen", + "display_fullpage": "Vollständige Seite Screenshot", + "display_visible": "Sichtbarer Bereich Screenshot" + }, + "pagination": { + "title": "Wie können wir das nächste Listenelement auf der Seite finden?", + "click_next": "Auf 'Weiter' klicken, um zur nächsten Seite zu navigieren", + "click_load_more": "Auf 'Mehr laden' klicken, um weitere Elemente zu laden", + "scroll_down": "Nach unten scrollen, um mehr Elemente zu laden", + "scroll_up": "Nach oben scrollen, um mehr Elemente zu laden", + "none": "Keine weiteren Elemente zu laden" + }, + "limit": { + "title": "Wie viele Zeilen möchten Sie maximal extrahieren?", + "custom": "Benutzerdefiniert", + "enter_number": "Nummer eingeben" + }, + "fields": { + "label": "Bezeichnung", + "data": "Daten", + "field_label": "Feldbezeichnung", + "field_data": "Felddaten" + }, + "messages": { + "list_selected": "Liste erfolgreich ausgewählt" + }, + "errors": { + "select_pagination": "Bitte wählen Sie einen Paginierungstyp aus.", + "select_pagination_element": "Bitte wählen Sie zuerst das Paginierungselement aus.", + "select_limit": "Bitte wählen Sie ein Limit oder geben Sie ein benutzerdefiniertes Limit ein.", + "invalid_limit": "Bitte geben Sie ein gültiges Limit ein.", + "confirm_text_fields": "Bitte bestätigen Sie alle Textfelder", + "unable_create_settings": "Listeneinstellungen können nicht erstellt werden. Stellen Sie sicher, dass Sie ein Feld für die Liste definiert haben.", + "capture_text_discarded": "Texterfassung verworfen", + "capture_list_discarded": "Listenerfassung verworfen" + } + }, + "save_recording": { + "title": "Roboter speichern", + "robot_name": "Roboter Name", + "buttons": { + "save": "Speichern", + "confirm": "Bestätigen" + }, + "notifications": { + "save_success": "Roboter erfolgreich gespeichert" + }, + "errors": { + "user_not_logged": "Benutzer nicht angemeldet. Aufnahme kann nicht gespeichert werden.", + "exists_warning": "Ein Roboter mit diesem Namen existiert bereits, bitte bestätigen Sie das Überschreiben des Roboters." + }, + "tooltips": { + "saving": "Workflow wird optimiert und gespeichert" + } + }, + "browser_recording": { + "modal": { + "confirm_discard": "Sind Sie sicher, dass Sie die Aufnahme verwerfen möchten?" + }, + "notifications": { + "terminated": "Aktuelle Aufnahme wurde beendet" + } + }, + "interpretation_log": { + "titles": { + "output_preview": "Vorschau der Ausgabedaten", + "screenshot": "Bildschirmfoto" + }, + "messages": { + "additional_rows": "Weitere Datenzeilen werden nach Abschluss der Aufnahme extrahiert.", + "successful_training": "Sie haben den Roboter erfolgreich für Aktionen trainiert! Klicken Sie auf die Schaltfläche unten, um eine Vorschau der Daten zu erhalten, die Ihr Roboter extrahieren wird.", + "no_selection": "Sie haben noch nichts zur Extraktion ausgewählt. Sobald Sie dies tun, wird der Roboter hier eine Vorschau Ihrer Auswahl anzeigen." + }, + "data_sections": { + "binary_received": "---------- Binäre Ausgabedaten empfangen ----------", + "serializable_received": "---------- Serialisierbare Ausgabedaten empfangen ----------", + "mimetype": "Medientyp: ", + "image_below": "Bild wird unten angezeigt:", + "separator": "--------------------------------------------------" + } + }, + "interpretation_buttons": { + "buttons": { + "preview": "Vorschau der Ausgabedaten anzeigen", + "yes": "Ja", + "no": "Nein" + }, + "messages": { + "extracting": "Daten werden extrahiert...bitte warten Sie 10 Sekunden bis 1 Minute", + "restart_required": "Bitte starten Sie die Interpretation nach der Aktualisierung der Aufnahme neu", + "run_finished": "Durchlauf beendet", + "run_failed": "Start fehlgeschlagen" + }, + "modal": { + "use_previous": "Möchten Sie Ihre vorherige Auswahl als Bedingung für diese Aktion verwenden?", + "previous_action": "Ihre vorherige Aktion war: ", + "element_text": "auf einem Element mit Text " + } + }, + "recording_page": { + "loader": { + "browser_startup": "Browser wird gestartet...Navigation zu {{url}}" + } + }, + "integration_settings": { + "title": "Mit Google Sheet integrieren", + "descriptions": { + "sync_info": "Wenn Sie diese Option aktivieren, werden bei jeder erfolgreichen Ausführung dieses Roboters die erfassten Daten in Ihrem Google Sheet ergänzt.", + "authenticated_as": "Authentifiziert als: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheet erfolgreich integriert.", + "content": "Jedes Mal, wenn dieser Roboter eine erfolgreiche Ausführung erstellt, werden die erfassten Daten Ihrem Google Sheet {{sheetName}} hinzugefügt. Sie können die Datenaktualisierungen", + "here": "hier", + "note": "Hinweis:", + "sync_limitation": "Die vor der Integration mit Google Sheets extrahierten Daten werden nicht im Google Sheet synchronisiert. Nur die nach der Integration extrahierten Daten werden synchronisiert." + } + }, + "buttons": { + "authenticate": "Mit Google authentifizieren", + "fetch_sheets": "Google Sheets abrufen", + "remove_integration": "Integration entfernen", + "submit": "Absenden" + }, + "fields": { + "select_sheet": "Google Sheet auswählen", + "selected_sheet": "Ausgewähltes Sheet: {{name}} (ID: {{id}})" + } + }, + "robot_duplication": { + "title": "Roboter duplizieren", + "descriptions": { + "purpose": "Die Roboter-Duplizierung ist nützlich, um Daten von Seiten mit der gleichen Struktur zu extrahieren.", + "example": "Beispiel: Wenn Sie einen Roboter für {{url1}} erstellt haben, können Sie ihn duplizieren, um ähnliche Seiten wie {{url2}} zu durchsuchen, ohne einen Roboter von Grund auf neu zu trainieren.", + "warning": "⚠️ Stellen Sie sicher, dass die neue Seite die gleiche Struktur wie die Originalseite hat." + }, + "fields": { + "target_url": "Roboter Ziel-URL" + }, + "buttons": { + "duplicate": "Roboter duplizieren", + "cancel": "Abbrechen" + }, + "notifications": { + "robot_not_found": "Roboterdetails konnten nicht gefunden werden. Bitte versuchen Sie es erneut.", + "url_required": "Ziel-URL ist erforderlich.", + "duplicate_success": "Roboter erfolgreich dupliziert.", + "duplicate_error": "Fehler beim Aktualisieren der Ziel-URL. Bitte versuchen Sie es erneut.", + "unknown_error": "Beim Aktualisieren der Ziel-URL ist ein Fehler aufgetreten." + } + }, + "robot_settings": { + "title": "Roboter-Einstellungen", + "target_url": "Roboter-Ziel-URL", + "robot_id": "Roboter-ID", + "robot_limit": "Roboter-Limit", + "created_by_user": "Erstellt von Benutzer", + "created_at": "Erstellungsdatum des Roboters", + "errors": { + "robot_not_found": "Roboterdetails konnten nicht gefunden werden. Bitte versuchen Sie es erneut." + } + }, + "robot_edit": { + "title": "Roboter bearbeiten", + "change_name": "Roboternamen ändern", + "robot_limit": "Roboter-Limit", + "save": "Änderungen speichern", + "cancel": "Abbrechen", + "notifications": { + "update_success": "Roboter erfolgreich aktualisiert.", + "update_failed": "Aktualisierung des Roboters fehlgeschlagen. Bitte erneut versuchen.", + "update_error": "Beim Aktualisieren des Roboters ist ein Fehler aufgetreten." + } + }, + "schedule_settings": { + "title": "Zeitplan-Einstellungen", + "run_every": "Ausführen alle", + "start_from": "Beginnen ab", + "on_day": "An Tag", + "at_around": "Um", + "timezone": "Zeitzone", + "buttons": { + "delete_schedule": "Zeitplan löschen", + "save_schedule": "Zeitplan speichern", + "cancel": "Abbrechen" + }, + "labels": { + "in_between": "Zwischen", + "run_once_every": "Ausführen alle", + "start_from_label": "Beginnen ab", + "on_day_of_month": "Tag des Monats", + "on_day": { + "st": ".", + "nd": ".", + "rd": ".", + "th": "." + } + } + }, + "main_page": { + "notifications": { + "interpretation_success": "Interpretation des Roboters {{name}} erfolgreich", + "interpretation_failed": "Interpretation des Roboters {{name}} fehlgeschlagen", + "run_started": "Roboter wird ausgeführt: {{name}}", + "run_start_failed": "Fehler beim Ausführen des Roboters: {{name}}", + "schedule_success": "Roboter {{name}} erfolgreich geplant", + "schedule_failed": "Planen des Roboters {{name}} fehlgeschlagen", + "abort_success": "Interpretation des Roboters {{name}} erfolgreich abgebrochen", + "abort_failed": "Abbrechen der Interpretation des Roboters {{name}} fehlgeschlagen" + }, + "menu": { + "recordings": "Roboter", + "runs": "Ausführungen", + "proxy": "Proxy", + "apikey": "API-Schlüssel" + } + }, + "browser_window": { + "attribute_modal": { + "title": "Attribut auswählen", + "notifications": { + "list_select_success": "Liste erfolgreich ausgewählt. Wählen Sie die zu extrahierenden Textdaten.", + "pagination_select_success": "Paginierungselement erfolgreich ausgewählt." + } + }, + "attribute_options": { + "anchor": { + "text": "Text: {{text}}", + "url": "URL: {{url}}" + }, + "image": { + "alt_text": "Alt-Text: {{altText}}", + "image_url": "Bild-URL: {{imageUrl}}" + }, + "default": { + "text": "Text: {{text}}" + } + } + }, + "runs_table": { + "run_type_chips": { + "manual_run": "Manuelle Ausführung", + "scheduled_run": "Geplante Ausführung", + "api": "API", + "unknown_run_type": "Unbekannter Ausführungstyp" + }, + "run_status_chips": { + "success": "Erfolg", + "running": "Läuft", + "scheduled": "Geplant", + "failed": "Fehlgeschlagen" + }, + "run_settings_modal": { + "title": "Ausführungseinstellungen", + "labels": { + "run_id": "Ausführungs-ID", + "run_by_user": "Ausgeführt von Benutzer", + "run_by_schedule": "Ausgeführt nach Zeitplan-ID", + "run_by_api": "Ausgeführt durch API", + "run_type": "Ausführungstyp" + } + } + }, + "run_content": { + "tabs": { + "output_data": "Ausgabedaten", + "log": "Protokoll" + }, + "empty_output": "Die Ausgabe ist leer.", + "captured_data": { + "title": "Erfasste Daten", + "download_json": "Als JSON herunterladen", + "download_csv": "Als CSV herunterladen" + }, + "captured_screenshot": { + "title": "Erfasster Screenshot", + "download": "Screenshot herunterladen", + "render_failed": "Das Bild konnte nicht gerendert werden" + }, + "buttons": { + "stop": "Stoppen" + } + }, + "navbar": { + "project_name": "Maxun", + "upgrade": { + "button": "Upgrade", + "modal": { + "up_to_date": "🎉 Du bist auf dem neuesten Stand!", + "new_version_available": "Eine neue Version ist verfügbar: {{version}}. Aktualisieren Sie auf die neueste Version für Fehlerkorrekturen, Verbesserungen und neue Funktionen!", + "view_updates": "Alle Updates anzeigen", + "view_updates_link": "hier", + "tabs": { + "manual_setup": "Manuelles Setup-Upgrade", + "docker_setup": "Docker Compose Setup-Upgrade" + } + } + }, + "menu_items": { + "logout": "Abmelden", + "discord": "Discord", + "youtube": "YouTube", + "twitter": "Twitter (X)", + "language": "Sprache" + }, + "recording": { + "discard": "Verwerfen" + } + }, + "language_menu": { + "en": "Englisch", + "es": "Spanisch", + "ja": "Japanisch", + "zh": "Chinesisch", + "de": "Deutsch" + } +} diff --git a/public/locales/en.json b/public/locales/en.json new file mode 100644 index 00000000..8084b5f2 --- /dev/null +++ b/public/locales/en.json @@ -0,0 +1,496 @@ +{ + "login": { + "title": "Welcome Back!", + "email": "Email", + "password": "Password", + "button": "Login", + "loading": "Loading", + "register_prompt": "Don't have an account?", + "register_link": "Register", + "welcome_notification": "Welcome to Maxun!", + "error_notification": "Login Failed. Please try again." + }, + "register": { + "title": "Register Account", + "email": "Email", + "password": "Password", + "button": "Register", + "loading": "Loading", + "register_prompt": "Already have an account?", + "login_link": "Login", + "welcome_notification": "Welcome to Maxun!", + "error_notification": "Registeration Failed. Please try again." + }, + "recordingtable":{ + "run": "Run", + "name": "Name", + "schedule": "Schedule", + "integrate": "Integrate", + "settings": "Settings", + "options": "Options", + "heading":"My Robots", + "new":"Create Robot", + "modal":{ + "title":"Enter the URL", + "label":"URL", + "button":"Start Recording" + }, + "edit":"Edit", + "delete":"Delete", + "duplicate":"Duplicate", + "search":"Search Robots...", + "notifications": { + "delete_warning": "Cannot delete robot as it has associated runs", + "delete_success": "Robot deleted successfully" + } + }, + "mainmenu":{ + "recordings": "Robots", + "runs": "Runs", + "proxy": "Proxy", + "apikey": "API Key", + "feedback":"Join Maxun Cloud", + "apidocs":"Website To API" + }, + "runstable":{ + "runs":"All Runs", + "runStatus":"Status", + "runName":"Name", + "startedAt":"Started At", + "finishedAt":"Finished At", + "delete":"Delete", + "settings":"Settings", + "search":"Search Runs...", + "notifications": { + "no_runs": "No runs found. Please try again.", + "delete_success": "Run deleted successfully" + } + }, + "proxy": { + "title": "Proxy Configuration", + "tab_standard": "Standard Proxy", + "tab_rotation": "Automatic Proxy Rotation", + "server_url": "Proxy Server URL", + "server_url_helper": "Proxy to be used for all robots. HTTP and SOCKS proxies are supported. Example http://myproxy.com:3128 or socks5://myproxy.com:3128. Short form myproxy.com:3128 is considered an HTTP proxy.", + "requires_auth": "Requires Authentication?", + "username": "Username", + "password": "Password", + "add_proxy": "Add Proxy", + "test_proxy": "Test Proxy", + "remove_proxy": "Remove Proxy", + "table": { + "proxy_url": "Proxy URL", + "requires_auth": "Requires Authentication" + }, + "coming_soon": "Coming Soon - In Open Source (Basic Rotation) & Cloud (Advanced Rotation). If you don't want to manage the infrastructure, join our cloud waitlist to get early access.", + "join_waitlist": "Join Maxun Cloud Waitlist", + "alert": { + "title": "If your proxy requires a username and password, always provide them separately from the proxy URL.", + "right_way": "The right way", + "wrong_way": "The wrong way", + "proxy_url": "Proxy URL:", + "username": "Username:", + "password": "Password:" + }, + "notifications": { + "config_success": "Proxy configuration submitted successfully", + "config_error": "Failed to submit proxy configuration. Try again.", + "test_success": "Proxy configuration is working", + "test_error": "Failed to test proxy configuration. Try again.", + "fetch_success": "Proxy configuration fetched successfully", + "remove_success": "Proxy configuration removed successfully", + "remove_error": "Failed to remove proxy configuration. Try again." + } + }, + "apikey": { + "title": "Manage Your API Key", + "default_name": "Maxun API Key", + "table": { + "name": "API Key Name", + "key": "API Key", + "actions": "Actions" + }, + "actions": { + "copy": "Copy", + "show": "Show", + "hide": "Hide", + "delete": "Delete" + }, + "no_key_message": "You haven't generated an API key yet.", + "generate_button": "Generate API Key", + "notifications": { + "fetch_error": "Failed to fetch API Key - ${error}", + "generate_success": "Generated API Key successfully", + "generate_error": "Failed to generate API Key - ${error}", + "delete_success": "API Key deleted successfully", + "delete_error": "Failed to delete API Key - ${error}", + "copy_success": "Copied API Key successfully" + } + }, + "action_description": { + "text": { + "title": "Capture Text", + "description": "Hover over the texts you want to extract and click to select them" + }, + "screenshot": { + "title": "Capture Screenshot", + "description": "Capture a partial or full page screenshot of the current page." + }, + "list": { + "title": "Capture List", + "description": "Hover over the list you want to extract. Once selected, you can hover over all texts inside the list you selected. Click to select them." + }, + "default": { + "title": "What data do you want to extract?", + "description": "A robot is designed to perform one action at a time. You can choose any of the options below." + }, + "list_stages": { + "initial": "Select the list you want to extract along with the texts inside it", + "pagination": "Select how the robot can capture the rest of the list", + "limit": "Choose the number of items to extract", + "complete": "Capture is complete" + } + }, + "right_panel": { + "buttons": { + "capture_list": "Capture List", + "capture_text": "Capture Text", + "capture_screenshot": "Capture Screenshot", + "confirm": "Confirm", + "discard": "Discard", + "confirm_capture": "Confirm Capture", + "confirm_pagination": "Confirm Pagination", + "confirm_limit": "Confirm Limit", + "finish_capture": "Finish Capture", + "finish": "Finish", + "cancel": "Cancel" + }, + "screenshot": { + "capture_fullpage": "Capture Fullpage", + "capture_visible": "Capture Visible Part", + "display_fullpage": "Take Fullpage Screenshot", + "display_visible": "Take Visible Part Screenshot" + }, + "pagination": { + "title": "How can we find the next list item on the page?", + "click_next": "Click on next to navigate to the next page", + "click_load_more": "Click on load more to load more items", + "scroll_down": "Scroll down to load more items", + "scroll_up": "Scroll up to load more items", + "none": "No more items to load" + }, + "limit": { + "title": "What is the maximum number of rows you want to extract?", + "custom": "Custom", + "enter_number": "Enter number" + }, + "fields": { + "label": "Label", + "data": "Data", + "field_label": "Field Label", + "field_data": "Field Data" + }, + "messages": { + "list_selected": "List Selected Successfully" + }, + "errors": { + "select_pagination": "Please select a pagination type.", + "select_pagination_element": "Please select the pagination element first.", + "select_limit": "Please select a limit or enter a custom limit.", + "invalid_limit": "Please enter a valid limit.", + "confirm_text_fields": "Please confirm all text fields", + "unable_create_settings": "Unable to create list settings. Make sure you have defined a field for the list.", + "capture_text_discarded": "Capture Text Discarded", + "capture_list_discarded": "Capture List Discarded" + } + }, + "save_recording": { + "title": "Save Robot", + "robot_name": "Robot Name", + "buttons": { + "save": "Save", + "confirm": "Confirm" + }, + "notifications": { + "save_success": "Robot saved successfully" + }, + "errors": { + "user_not_logged": "User not logged in. Cannot save recording.", + "exists_warning": "Robot with this name already exists, please confirm the Robot's overwrite." + }, + "tooltips": { + "saving": "Optimizing and saving the workflow" + } + }, + "browser_recording": { + "modal": { + "confirm_discard": "Are you sure you want to discard the recording?" + }, + "notifications": { + "terminated": "Current Recording was terminated" + } + }, + "interpretation_log": { + "titles": { + "output_preview": "Output Data Preview", + "screenshot": "Screenshot" + }, + "messages": { + "additional_rows": "Additional rows of data will be extracted once you finish recording.", + "successful_training": "You've successfully trained the robot to perform actions! Click on the button below to get a preview of the data your robot will extract.", + "no_selection": "It looks like you have not selected anything for extraction yet. Once you do, the robot will show a preview of your selections here." + }, + "data_sections": { + "binary_received": "---------- Binary output data received ----------", + "serializable_received": "---------- Serializable output data received ----------", + "mimetype": "mimetype: ", + "image_below": "Image is rendered below:", + "separator": "--------------------------------------------------" + } + }, + "interpretation_buttons": { + "buttons": { + "preview": "Get Preview of Output Data", + "yes": "Yes", + "no": "No" + }, + "messages": { + "extracting": "Extracting data...please wait for 10secs to 1min", + "restart_required": "Please restart the interpretation after updating the recording", + "run_finished": "Run finished", + "run_failed": "Run failed to start" + }, + "modal": { + "use_previous": "Do you want to use your previous selection as a condition for performing this action?", + "previous_action": "Your previous action was: ", + "element_text": "on an element with text " + } + }, + "recording_page": { + "loader": { + "browser_startup": "Spinning up a browser...Navigating to {{url}}" + } + }, + "integration_settings": { + "title": "Integrate with Google Sheet", + "descriptions": { + "sync_info": "If you enable this option, every time this robot runs a task successfully, its captured data will be appended to your Google Sheet.", + "authenticated_as": "Authenticated as: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheet Integrated Successfully.", + "content": "Every time this robot creates a successful run, its captured data is appended to your {{sheetName}} Google Sheet. You can check the data updates", + "here": "here", + "note": "Note:", + "sync_limitation": "The data extracted before integrating with Google Sheets will not be synced in the Google Sheet. Only the data extracted after the integration will be synced." + } + }, + "buttons": { + "authenticate": "Authenticate with Google", + "fetch_sheets": "Fetch Google Spreadsheets", + "remove_integration": "Remove Integration", + "submit": "Submit" + }, + "fields": { + "select_sheet": "Select Google Sheet", + "selected_sheet": "Selected Sheet: {{name}} (ID: {{id}})" + }, + "errors": { + "auth_error": "Error authenticating with Google", + "fetch_error": "Error fetching spreadsheet files: {{message}}", + "update_error": "Error updating Google Sheet ID: {{message}}", + "remove_error": "Error removing Google Sheets integration: {{message}}" + }, + "notifications": { + "sheet_selected": "Google Sheet selected successfully" + } + }, + "robot_duplication": { + "title": "Duplicate Robot", + "descriptions": { + "purpose": "Robot duplication is useful to extract data from pages with the same structure.", + "example": "Example: If you've created a robot for {{url1}}, you can duplicate it to scrape similar pages like {{url2}} without training a robot from scratch.", + "warning": "⚠️ Ensure the new page has the same structure as the original page." + }, + "fields": { + "target_url": "Robot Target URL" + }, + "buttons": { + "duplicate": "Duplicate Robot", + "cancel": "Cancel" + }, + "notifications": { + "robot_not_found": "Could not find robot details. Please try again.", + "url_required": "Target URL is required.", + "duplicate_success": "Robot duplicated successfully.", + "duplicate_error": "Failed to update the Target URL. Please try again.", + "unknown_error": "An error occurred while updating the Target URL." + } + }, + "robot_settings": { + "title": "Robot Settings", + "target_url": "Robot Target URL", + "robot_id": "Robot ID", + "robot_limit": "Robot Limit", + "created_by_user": "Created By User", + "created_at": "Robot Created At", + "errors": { + "robot_not_found": "Could not find robot details. Please try again." + } + }, + "robot_edit": { + "title": "Edit Robot", + "change_name": "Change Robot Name", + "robot_limit": "Robot Limit", + "save": "Save Changes", + "cancel": "Cancel", + "notifications": { + "update_success": "Robot updated successfully.", + "update_failed": "Failed to update the robot. Please try again.", + "update_error": "An error occurred while updating the robot." + } + }, + "schedule_settings": { + "title": "Schedule Settings", + "run_every": "Run every", + "start_from": "Start From", + "on_day": "On day", + "at_around": "At around", + "timezone": "Timezone", + "buttons": { + "delete_schedule": "Delete Schedule", + "save_schedule": "Save Schedule", + "cancel": "Cancel" + }, + "labels": { + "in_between": "In Between", + "run_once_every": "Run once every", + "start_from_label": "Start From", + "on_day_of_month": "On Day of the Month", + "on_day": { + "st": "st", + "nd": "nd", + "rd": "rd", + "th": "th" + } + } + }, + "main_page": { + "notifications": { + "interpretation_success": "Interpretation of robot {{name}} succeeded", + "interpretation_failed": "Failed to interpret robot {{name}}", + "run_started": "Running robot: {{name}}", + "run_start_failed": "Failed to run robot: {{name}}", + "schedule_success": "Robot {{name}} scheduled successfully", + "schedule_failed": "Failed to schedule robot {{name}}", + "abort_success": "Interpretation of robot {{name}} aborted successfully", + "abort_failed": "Failed to abort the interpretation of robot {{name}}" + }, + "menu": { + "recordings": "Robots", + "runs": "Runs", + "proxy": "Proxy", + "apikey": "API Key" + } + }, + "browser_window": { + "attribute_modal": { + "title": "属性を選択", + "notifications": { + "list_select_success": "リストが正常に選択されました。抽出するテキストデータを選択してください。", + "pagination_select_success": "ページネーション要素が正常に選択されました。" + } + }, + "attribute_options": { + "anchor": { + "text": "テキスト: {{text}}", + "url": "URL: {{url}}" + }, + "image": { + "alt_text": "代替テキスト: {{altText}}", + "image_url": "画像URL: {{imageUrl}}" + }, + "default": { + "text": "テキスト: {{text}}" + } + } + }, + "runs_table": { + "run_type_chips": { + "manual_run": "Manual Run", + "scheduled_run": "Scheduled Run", + "api": "API", + "unknown_run_type": "Unknown Run Type" + }, + "run_status_chips": { + "success": "Success", + "running": "Running", + "scheduled": "Scheduled", + "failed": "Failed" + }, + "run_settings_modal": { + "title": "Run Settings", + "labels": { + "run_id": "Run ID", + "run_by_user": "Run by User", + "run_by_schedule": "Run by Schedule ID", + "run_by_api": "Run by API", + "run_type": "Run Type" + } + } + }, + "run_content": { + "tabs": { + "output_data": "Output Data", + "log": "Log" + }, + "empty_output": "The output is empty.", + "captured_data": { + "title": "Captured Data", + "download_json": "Download as JSON", + "download_csv": "Download as CSV" + }, + "captured_screenshot": { + "title": "Captured Screenshot", + "download": "Download Screenshot", + "render_failed": "The image failed to render" + }, + "buttons": { + "stop": "Stop" + } + }, + "navbar": { + "project_name": "Maxun", + "upgrade": { + "button": "Upgrade", + "modal": { + "up_to_date": "🎉 You're up to date!", + "new_version_available": "A new version is available: {{version}}. Upgrade to the latest version for bug fixes, enhancements and new features!", + "view_updates": "View all the new updates", + "view_updates_link": "here", + "tabs": { + "manual_setup": "Manual Setup Upgrade", + "docker_setup": "Docker Compose Setup Upgrade" + } + } + }, + "menu_items": { + "logout": "Logout", + "discord": "Discord", + "youtube": "YouTube", + "twitter": "Twitter (X)", + "language": "Language" + }, + "recording": { + "discard": "Discard" + } + }, + "language_menu": { + "en": "English", + "es": "Spanish", + "ja": "Japanese", + "zh": "Chinese", + "de": "German" + } +} \ No newline at end of file diff --git a/public/locales/es.json b/public/locales/es.json new file mode 100644 index 00000000..4ef417ea --- /dev/null +++ b/public/locales/es.json @@ -0,0 +1,487 @@ +{ + "login": { + "title": "¡Bienvenido de nuevo!", + "email": "Correo electrónico", + "password": "Contraseña", + "button": "Iniciar sesión", + "loading": "Cargando", + "register_prompt": "¿No tienes una cuenta?", + "register_link": "Registrarse", + "welcome_notification": "¡Bienvenido a Maxun!", + "error_notification": "Error al iniciar sesión. Por favor, inténtalo de nuevo." + }, + "register": { + "title": "Crear cuenta", + "email": "Correo electrónico", + "password": "Contraseña", + "button": "Registrarse", + "loading": "Cargando", + "register_prompt": "¿Ya tienes una cuenta?", + "login_link": "Iniciar sesión", + "welcome_notification": "¡Bienvenido a Maxun!", + "error_notification": "Error en el registro. Por favor, inténtalo de nuevo." + }, + "recordingtable": { + "run": "Ejecutar", + "name": "Nombre", + "schedule": "Programar", + "integrate": "Integrar", + "settings": "Ajustes", + "options": "Opciones", + "heading": "Mis Robots", + "new": "Crear Robot", + "modal": { + "title": "Ingresa la URL", + "label": "URL", + "button": "Comenzar grabación" + }, + "edit": "Editar", + "delete": "Eliminar", + "duplicate": "Duplicar", + "search": "Buscar robots...", + "notifications": { + "delete_warning": "No se puede eliminar el robot ya que tiene ejecuciones asociadas", + "delete_success": "Robot eliminado exitosamente" + } + }, + "mainmenu": { + "recordings": "Robots", + "runs": "Ejecuciones", + "proxy": "Proxy", + "apikey": "Clave API", + "feedback": "Unirse a Maxun Cloud", + "apidocs": "Sitio Web a API" + }, + "runstable": { + "runs": "Todas las ejecuciones", + "runStatus": "Estado", + "runName": "Nombre", + "startedAt": "Iniciado el", + "finishedAt": "Finalizado el", + "delete": "Eliminar", + "settings": "Ajustes", + "search": "Buscar ejecuciones...", + "notifications": { + "no_runs": "No se encontraron ejecuciones. Por favor, inténtelo de nuevo.", + "delete_success": "Ejecución eliminada con éxito" + } + }, + "proxy": { + "title": "Configuración del Proxy", + "tab_standard": "Proxy Estándar", + "tab_rotation": "Rotación Automática de Proxy", + "server_url": "URL del Servidor Proxy", + "server_url_helper": "Proxy para usar en todos los robots. Se admiten proxies HTTP y SOCKS. Ejemplo http://myproxy.com:3128 o socks5://myproxy.com:3128. La forma corta myproxy.com:3128 se considera un proxy HTTP.", + "requires_auth": "¿Requiere Autenticación?", + "username": "Usuario", + "password": "Contraseña", + "add_proxy": "Agregar Proxy", + "test_proxy": "Probar Proxy", + "remove_proxy": "Eliminar Proxy", + "table": { + "proxy_url": "URL del Proxy", + "requires_auth": "Requiere Autenticación" + }, + "coming_soon": "Próximamente - En Open Source (Rotación Básica) y Cloud (Rotación Avanzada). Si no desea administrar la infraestructura, únase a nuestra lista de espera en la nube para obtener acceso anticipado.", + "join_waitlist": "Unirse a la Lista de Espera de Maxun Cloud", + "alert": { + "title": "Si su proxy requiere un nombre de usuario y contraseña, proporcione siempre estos datos por separado de la URL del proxy.", + "right_way": "La forma correcta", + "wrong_way": "La forma incorrecta", + "proxy_url": "URL del Proxy:", + "username": "Usuario:", + "password": "Contraseña:" + }, + "notifications": { + "config_success": "Configuración del proxy enviada con éxito", + "config_error": "Error al enviar la configuración del proxy. Inténtelo de nuevo.", + "test_success": "La configuración del proxy funciona correctamente", + "test_error": "Error al probar la configuración del proxy. Inténtelo de nuevo.", + "fetch_success": "Configuración del proxy recuperada con éxito", + "remove_success": "Configuración del proxy eliminada con éxito", + "remove_error": "Error al eliminar la configuración del proxy. Inténtelo de nuevo." + } + }, + "apikey": { + "title": "Gestionar tu Clave API", + "default_name": "Clave API de Maxun", + "table": { + "name": "Nombre de la Clave API", + "key": "Clave API", + "actions": "Acciones" + }, + "actions": { + "copy": "Copiar", + "show": "Mostrar", + "hide": "Ocultar", + "delete": "Eliminar" + }, + "no_key_message": "Aún no has generado una clave API.", + "generate_button": "Generar Clave API", + "notifications": { + "fetch_error": "Error al obtener la clave API - ${error}", + "generate_success": "Clave API generada con éxito", + "generate_error": "Error al generar la clave API - ${error}", + "delete_success": "Clave API eliminada con éxito", + "delete_error": "Error al eliminar la clave API - ${error}", + "copy_success": "Clave API copiada con éxito" + } + }, + "action_description": { + "text": { + "title": "Capturar Texto", + "description": "Pase el cursor sobre los textos que desea extraer y haga clic para seleccionarlos" + }, + "screenshot": { + "title": "Capturar Pantalla", + "description": "Capture una captura de pantalla parcial o completa de la página actual." + }, + "list": { + "title": "Capturar Lista", + "description": "Pase el cursor sobre la lista que desea extraer. Una vez seleccionada, puede pasar el cursor sobre todos los textos dentro de la lista seleccionada. Haga clic para seleccionarlos." + }, + "default": { + "title": "¿Qué datos desea extraer?", + "description": "Un robot está diseñado para realizar una acción a la vez. Puede elegir cualquiera de las siguientes opciones." + }, + "list_stages": { + "initial": "Seleccione la lista que desea extraer junto con los textos que contiene", + "pagination": "Seleccione cómo puede el robot capturar el resto de la lista", + "limit": "Elija el número de elementos a extraer", + "complete": "Captura completada" + } + }, + "right_panel": { + "buttons": { + "capture_list": "Capturar Lista", + "capture_text": "Capturar Texto", + "capture_screenshot": "Capturar Pantalla", + "confirm": "Confirmar", + "discard": "Descartar", + "confirm_capture": "Confirmar Captura", + "confirm_pagination": "Confirmar Paginación", + "confirm_limit": "Confirmar Límite", + "finish_capture": "Finalizar Captura", + "finish": "Finalizar", + "cancel": "Cancelar" + }, + "screenshot": { + "capture_fullpage": "Capturar Página Completa", + "capture_visible": "Capturar Parte Visible", + "display_fullpage": "Capturar Screenshot de Página Completa", + "display_visible": "Capturar Screenshot de Parte Visible" + }, + "pagination": { + "title": "¿Cómo podemos encontrar el siguiente elemento de la lista en la página?", + "click_next": "Hacer clic en siguiente para navegar a la siguiente página", + "click_load_more": "Hacer clic en cargar más para cargar más elementos", + "scroll_down": "Desplazarse hacia abajo para cargar más elementos", + "scroll_up": "Desplazarse hacia arriba para cargar más elementos", + "none": "No hay más elementos para cargar" + }, + "limit": { + "title": "¿Cuál es el número máximo de filas que desea extraer?", + "custom": "Personalizado", + "enter_number": "Ingrese número" + }, + "fields": { + "label": "Etiqueta", + "data": "Datos", + "field_label": "Etiqueta del Campo", + "field_data": "Datos del Campo" + }, + "messages": { + "list_selected": "Lista seleccionada exitosamente" + }, + "errors": { + "select_pagination": "Por favor seleccione un tipo de paginación.", + "select_pagination_element": "Por favor seleccione primero el elemento de paginación.", + "select_limit": "Por favor seleccione un límite o ingrese un límite personalizado.", + "invalid_limit": "Por favor ingrese un límite válido.", + "confirm_text_fields": "Por favor confirme todos los campos de texto", + "unable_create_settings": "No se pueden crear las configuraciones de la lista. Asegúrese de haber definido un campo para la lista.", + "capture_text_discarded": "Captura de texto descartada", + "capture_list_discarded": "Captura de lista descartada" + } + }, + "save_recording": { + "title": "Guardar Robot", + "robot_name": "Nombre del Robot", + "buttons": { + "save": "Guardar", + "confirm": "Confirmar" + }, + "notifications": { + "save_success": "Robot guardado exitosamente" + }, + "errors": { + "user_not_logged": "Usuario no conectado. No se puede guardar la grabación.", + "exists_warning": "Ya existe un robot con este nombre, por favor confirme la sobrescritura del robot." + }, + "tooltips": { + "saving": "Optimizando y guardando el flujo de trabajo" + } + }, + "browser_recording": { + "modal": { + "confirm_discard": "¿Está seguro de que desea descartar la grabación?" + }, + "notifications": { + "terminated": "La grabación actual fue terminada" + } + }, + "interpretation_log": { + "titles": { + "output_preview": "Vista Previa de Datos de Salida", + "screenshot": "Captura de pantalla" + }, + "messages": { + "additional_rows": "Se extraerán filas adicionales de datos una vez que termine la grabación.", + "successful_training": "¡Has entrenado exitosamente al robot para realizar acciones! Haz clic en el botón de abajo para obtener una vista previa de los datos que tu robot extraerá.", + "no_selection": "Parece que aún no has seleccionado nada para extraer. Una vez que lo hagas, el robot mostrará una vista previa de tus selecciones aquí." + }, + "data_sections": { + "binary_received": "---------- Datos binarios de salida recibidos ----------", + "serializable_received": "---------- Datos serializables de salida recibidos ----------", + "mimetype": "tipo MIME: ", + "image_below": "La imagen se muestra a continuación:", + "separator": "--------------------------------------------------" + } + }, + "interpretation_buttons": { + "buttons": { + "preview": "Obtener Vista Previa de Datos de Salida", + "yes": "Sí", + "no": "No" + }, + "messages": { + "extracting": "Extrayendo datos...espere de 10 segundos a 1 minuto", + "restart_required": "Por favor, reinicie la interpretación después de actualizar la grabación", + "run_finished": "Ejecución finalizada", + "run_failed": "Error al iniciar la ejecución" + }, + "modal": { + "use_previous": "¿Desea usar su selección anterior como condición para realizar esta acción?", + "previous_action": "Su acción anterior fue: ", + "element_text": "en un elemento con texto " + } + }, + "recording_page": { + "loader": { + "browser_startup": "Iniciando el navegador...Navegando a {{url}}" + } + }, + "integration_settings": { + "title": "Integrar con Google Sheet", + "descriptions": { + "sync_info": "Si habilitas esta opción, cada vez que este robot ejecute una tarea exitosamente, sus datos capturados se añadirán a tu Google Sheet.", + "authenticated_as": "Autenticado como: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheet integrado exitosamente.", + "content": "Cada vez que este robot crea una ejecución exitosa, sus datos capturados se añaden a tu Google Sheet {{sheetName}}. Puedes verificar las actualizaciones de datos", + "here": "aquí", + "note": "Nota:", + "sync_limitation": "Los datos extraídos antes de la integración con Google Sheets no se sincronizarán en el Google Sheet. Solo se sincronizarán los datos extraídos después de la integración." + } + }, + "buttons": { + "authenticate": "Autenticar con Google", + "fetch_sheets": "Obtener Google Sheets", + "remove_integration": "Eliminar integración", + "submit": "Enviar" + }, + "fields": { + "select_sheet": "Seleccionar Google Sheet", + "selected_sheet": "Hoja seleccionada: {{name}} (ID: {{id}})" + } + }, + "robot_duplication": { + "title": "Duplicar Robot", + "descriptions": { + "purpose": "La duplicación de robots es útil para extraer datos de páginas con la misma estructura.", + "example": "Ejemplo: Si has creado un robot para {{url1}}, puedes duplicarlo para extraer páginas similares como {{url2}} sin tener que entrenar un robot desde cero.", + "warning": "⚠️ Asegúrate de que la nueva página tenga la misma estructura que la página original." + }, + "fields": { + "target_url": "URL Destino del Robot" + }, + "buttons": { + "duplicate": "Duplicar Robot", + "cancel": "Cancelar" + }, + "notifications": { + "robot_not_found": "No se pudieron encontrar los detalles del robot. Por favor, inténtalo de nuevo.", + "url_required": "Se requiere la URL de destino.", + "duplicate_success": "Robot duplicado con éxito.", + "duplicate_error": "Error al actualizar la URL de destino. Por favor, inténtalo de nuevo.", + "unknown_error": "Ocurrió un error al actualizar la URL de destino." + } + }, + "robot_settings": { + "title": "Configuración del Robot", + "target_url": "URL de Destino del Robot", + "robot_id": "ID del Robot", + "robot_limit": "Límite del Robot", + "created_by_user": "Creado por Usuario", + "created_at": "Fecha de Creación del Robot", + "errors": { + "robot_not_found": "No se pudieron encontrar los detalles del robot. Inténtelo de nuevo." + } + }, + "robot_edit": { + "title": "Editar Robot", + "change_name": "Cambiar Nombre del Robot", + "robot_limit": "Límite del Robot", + "save": "Guardar Cambios", + "cancel": "Cancelar", + "notifications": { + "update_success": "Robot actualizado exitosamente.", + "update_failed": "Error al actualizar el robot. Intente de nuevo.", + "update_error": "Ocurrió un error al actualizar el robot." + } + }, + "schedule_settings": { + "title": "Configuración de Programación", + "run_every": "Ejecutar cada", + "start_from": "Iniciar desde", + "on_day": "En día", + "at_around": "Alrededor de", + "timezone": "Zona horaria", + "buttons": { + "delete_schedule": "Eliminar Programación", + "save_schedule": "Guardar Programación", + "cancel": "Cancelar" + }, + "labels": { + "in_between": "Entre", + "run_once_every": "Ejecutar cada", + "start_from_label": "Iniciar desde", + "on_day_of_month": "Día del mes", + "on_day": { + "st": "º", + "nd": "º", + "rd": "º", + "th": "º" + } + } + }, + "main_page": { + "notifications": { + "interpretation_success": "Interpretación del robot {{name}} completada con éxito", + "interpretation_failed": "Error al interpretar el robot {{name}}", + "run_started": "Ejecutando robot: {{name}}", + "run_start_failed": "Error al ejecutar el robot: {{name}}", + "schedule_success": "Robot {{name}} programado exitosamente", + "schedule_failed": "Error al programar el robot {{name}}", + "abort_success": "Interpretación del robot {{name}} abortada exitosamente", + "abort_failed": "Error al abortar la interpretación del robot {{name}}" + }, + "menu": { + "recordings": "Robots", + "runs": "Ejecuciones", + "proxy": "Proxy", + "apikey": "Clave API" + } + }, + "browser_window": { + "attribute_modal": { + "title": "Seleccionar Atributo", + "notifications": { + "list_select_success": "Lista seleccionada correctamente. Seleccione los datos de texto para extracción.", + "pagination_select_success": "Elemento de paginación seleccionado correctamente." + } + }, + "attribute_options": { + "anchor": { + "text": "Texto: {{text}}", + "url": "URL: {{url}}" + }, + "image": { + "alt_text": "Texto Alt: {{altText}}", + "image_url": "URL de Imagen: {{imageUrl}}" + }, + "default": { + "text": "Texto: {{text}}" + } + } + }, + "runs_table": { + "run_type_chips": { + "manual_run": "Ejecución Manual", + "scheduled_run": "Ejecución Programada", + "api": "API", + "unknown_run_type": "Tipo de Ejecución Desconocido" + }, + "run_status_chips": { + "success": "Éxito", + "running": "Ejecutando", + "scheduled": "Programado", + "failed": "Fallido" + }, + "run_settings_modal": { + "title": "Configuración de Ejecución", + "labels": { + "run_id": "ID de Ejecución", + "run_by_user": "Ejecutado por Usuario", + "run_by_schedule": "Ejecutado por ID de Programación", + "run_by_api": "Ejecutado por API", + "run_type": "Tipo de Ejecución" + } + } + }, + "run_content": { + "tabs": { + "output_data": "Datos de Salida", + "log": "Registro" + }, + "empty_output": "La salida está vacía.", + "captured_data": { + "title": "Datos Capturados", + "download_json": "Descargar como JSON", + "download_csv": "Descargar como CSV" + }, + "captured_screenshot": { + "title": "Captura de Pantalla", + "download": "Descargar Captura", + "render_failed": "No se pudo renderizar la imagen" + }, + "buttons": { + "stop": "Detener" + } + }, + "navbar": { + "project_name": "Maxun", + "upgrade": { + "button": "Actualizar", + "modal": { + "up_to_date": "¡Estás actualizado!", + "new_version_available": "Hay una nueva versión disponible: {{version}}. ¡Actualice a la última versión para correcciones de errores, mejoras y nuevas características!", + "view_updates": "Ver todas las actualizaciones", + "view_updates_link": "aquí", + "tabs": { + "manual_setup": "Actualización de Configuración Manual", + "docker_setup": "Actualización de Configuración Docker Compose" + } + } + }, + "menu_items": { + "logout": "Cerrar sesión", + "discord": "Discord", + "youtube": "YouTube", + "twitter": "Twitter (X)", + "language": "Idioma" + }, + "recording": { + "discard": "Descartar" + } + }, + "language_menu": { + "en": "Inglés", + "es": "Español", + "ja": "Japonés", + "zh": "Chino", + "de": "Alemán" + } +} \ No newline at end of file diff --git a/public/locales/ja.json b/public/locales/ja.json new file mode 100644 index 00000000..b40cc6e6 --- /dev/null +++ b/public/locales/ja.json @@ -0,0 +1,487 @@ +{ + "login": { + "title": "お帰りなさい!", + "email": "メールアドレス", + "password": "パスワード", + "button": "ログイン", + "loading": "読み込み中", + "register_prompt": "アカウントをお持ちでないですか?", + "register_link": "登録する", + "welcome_notification": "Maxunへようこそ!", + "error_notification": "ログインに失敗しました。もう一度お試しください。" + }, + "register": { + "title": "アカウントを登録する", + "email": "メールアドレス", + "password": "パスワード", + "button": "登録する", + "loading": "読み込み中", + "register_prompt": "既にアカウントをお持ちですか?", + "login_link": "ログイン", + "welcome_notification": "Maxunへようこそ!", + "error_notification": "登録に失敗しました。もう一度お試しください。" + }, + "recordingtable": { + "run": "実行", + "name": "名前", + "schedule": "スケジュール", + "integrate": "統合", + "settings": "設定", + "options": "オプション", + "heading": "私のロボット", + "new": "ロボットを作成", + "modal": { + "title": "URLを入力してください", + "label": "URL", + "button": "録画を開始" + }, + "edit": "編集", + "delete": "削除", + "duplicate": "複製", + "search": "ロボットを検索...", + "notifications": { + "delete_warning": "関連する実行があるため、ロボットを削除できません", + "delete_success": "ロボットが正常に削除されました" + } + }, + "mainmenu": { + "recordings": "ロボット", + "runs": "実行", + "proxy": "プロキシ", + "apikey": "APIキー", + "feedback": "Maxunクラウドに参加する", + "apidocs": "WebサイトからAPI" + }, + "runstable": { + "runs": "すべての実行", + "runStatus": "ステータス", + "runName": "名前", + "startedAt": "開始日時", + "finishedAt": "終了日時", + "delete": "削除", + "settings": "設定", + "search": "実行を検索...", + "notifications": { + "no_runs": "実行が見つかりません。もう一度お試しください。", + "delete_success": "実行が正常に削除されました" + } + }, + "proxy": { + "title": "プロキシ設定", + "tab_standard": "標準プロキシ", + "tab_rotation": "自動プロキシローテーション", + "server_url": "プロキシサーバーURL", + "server_url_helper": "すべてのロボットで使用するプロキシ。HTTPとSOCKSプロキシがサポートされています。例:http://myproxy.com:3128 または socks5://myproxy.com:3128。短縮形 myproxy.com:3128 はHTTPプロキシとして扱われます。", + "requires_auth": "認証が必要ですか?", + "username": "ユーザー名", + "password": "パスワード", + "add_proxy": "プロキシを追加", + "test_proxy": "プロキシをテスト", + "remove_proxy": "プロキシを削除", + "table": { + "proxy_url": "プロキシURL", + "requires_auth": "認証が必要" + }, + "coming_soon": "近日公開 - オープンソース(基本ローテーション)とクラウド(高度なローテーション)。インフラストラクチャを管理したくない場合は、クラウドの待機リストに参加して早期アクセスを取得してください。", + "join_waitlist": "Maxun Cloud待機リストに参加", + "alert": { + "title": "プロキシにユーザー名とパスワードが必要な場合は、必ずプロキシURLとは別に指定してください。", + "right_way": "正しい方法", + "wrong_way": "間違った方法", + "proxy_url": "プロキシURL:", + "username": "ユーザー名:", + "password": "パスワード:" + }, + "notifications": { + "config_success": "プロキシ設定が正常に送信されました", + "config_error": "プロキシ設定の送信に失敗しました。もう一度お試しください。", + "test_success": "プロキシ設定は正常に動作しています", + "test_error": "プロキシ設定のテストに失敗しました。もう一度お試しください。", + "fetch_success": "プロキシ設定の取得に成功しました", + "remove_success": "プロキシ設定が正常に削除されました", + "remove_error": "プロキシ設定の削除に失敗しました。もう一度お試しください。" + } + }, + "apikey": { + "title": "APIキーの管理", + "default_name": "Maxun APIキー", + "table": { + "name": "APIキー名", + "key": "APIキー", + "actions": "アクション" + }, + "actions": { + "copy": "コピー", + "show": "表示", + "hide": "非表示", + "delete": "削除" + }, + "no_key_message": "APIキーはまだ生成されていません。", + "generate_button": "APIキーを生成", + "notifications": { + "fetch_error": "APIキーの取得に失敗しました - ${error}", + "generate_success": "APIキーが正常に生成されました", + "generate_error": "APIキーの生成に失敗しました - ${error}", + "delete_success": "APIキーが正常に削除されました", + "delete_error": "APIキーの削除に失敗しました - ${error}", + "copy_success": "APIキーがコピーされました" + } + }, + "action_description": { + "text": { + "title": "テキストを取得", + "description": "抽出したいテキストにカーソルを合わせ、クリックして選択してください" + }, + "screenshot": { + "title": "スクリーンショットを取得", + "description": "現在のページの部分的または全体のスクリーンショットを取得します。" + }, + "list": { + "title": "リストを取得", + "description": "抽出したいリストにカーソルを合わせてください。選択後、選択したリスト内のすべてのテキストにカーソルを合わせることができます。クリックして選択してください。" + }, + "default": { + "title": "どのデータを抽出しますか?", + "description": "ロボットは一度に1つのアクションを実行するように設計されています。以下のオプションから選択できます。" + }, + "list_stages": { + "initial": "抽出したいリストとその中のテキストを選択してください", + "pagination": "ロボットがリストの残りをどのように取得するか選択してください", + "limit": "抽出するアイテムの数を選択してください", + "complete": "取得が完了しました" + } + }, + "right_panel": { + "buttons": { + "capture_list": "リストを取得", + "capture_text": "テキストを取得", + "capture_screenshot": "スクリーンショットを取得", + "confirm": "確認", + "discard": "破棄", + "confirm_capture": "取得を確認", + "confirm_pagination": "ページネーションを確認", + "confirm_limit": "制限を確認", + "finish_capture": "取得を完了", + "finish": "完了", + "cancel": "キャンセル" + }, + "screenshot": { + "capture_fullpage": "フルページを取得", + "capture_visible": "表示部分を取得", + "display_fullpage": "フルページスクリーンショットを撮影", + "display_visible": "表示部分のスクリーンショットを撮影" + }, + "pagination": { + "title": "次のリスト項目をページ上でどのように見つけますか?", + "click_next": "次へをクリックして次のページへ移動", + "click_load_more": "もっと読み込むをクリックして項目を追加", + "scroll_down": "下にスクロールして項目を追加", + "scroll_up": "上にスクロールして項目を追加", + "none": "これ以上読み込む項目はありません" + }, + "limit": { + "title": "抽出する最大行数はいくつですか?", + "custom": "カスタム", + "enter_number": "数値を入力" + }, + "fields": { + "label": "ラベル", + "data": "データ", + "field_label": "フィールドラベル", + "field_data": "フィールドデータ" + }, + "messages": { + "list_selected": "リストが正常に選択されました" + }, + "errors": { + "select_pagination": "ページネーションタイプを選択してください。", + "select_pagination_element": "まずページネーション要素を選択してください。", + "select_limit": "制限を選択するかカスタム制限を入力してください。", + "invalid_limit": "有効な制限を入力してください。", + "confirm_text_fields": "すべてのテキストフィールドを確認してください", + "unable_create_settings": "リスト設定を作成できません。リストのフィールドを定義したことを確認してください。", + "capture_text_discarded": "テキスト取得が破棄されました", + "capture_list_discarded": "リスト取得が破棄されました" + } + }, + "save_recording": { + "title": "ロボットを保存", + "robot_name": "ロボット名", + "buttons": { + "save": "保存", + "confirm": "確認" + }, + "notifications": { + "save_success": "ロボットが正常に保存されました" + }, + "errors": { + "user_not_logged": "ユーザーがログインしていません。録画を保存できません。", + "exists_warning": "この名前のロボットは既に存在します。ロボットの上書きを確認してください。" + }, + "tooltips": { + "saving": "ワークフローを最適化して保存中" + } + }, + "browser_recording": { + "modal": { + "confirm_discard": "録画を破棄してもよろしいですか?" + }, + "notifications": { + "terminated": "現在の録画は終了しました" + } + }, + "interpretation_log": { + "titles": { + "output_preview": "出力データプレビュー", + "screenshot": "スクリーンショット" + }, + "messages": { + "additional_rows": "記録が完了すると、追加のデータ行が抽出されます。", + "successful_training": "ロボットのアクショントレーニングが成功しました!下のボタンをクリックすると、ロボットが抽出するデータのプレビューが表示されます。", + "no_selection": "まだ抽出対象が選択されていません。選択すると、ロボットがここで選択内容のプレビューを表示します。" + }, + "data_sections": { + "binary_received": "---------- バイナリ出力データを受信 ----------", + "serializable_received": "---------- シリアライズ可能な出力データを受信 ----------", + "mimetype": "MIMEタイプ: ", + "image_below": "画像は以下に表示されます:", + "separator": "--------------------------------------------------" + } + }, + "interpretation_buttons": { + "buttons": { + "preview": "出力データのプレビューを取得", + "yes": "はい", + "no": "いいえ" + }, + "messages": { + "extracting": "データ抽出中...10秒から1分ほどお待ちください", + "restart_required": "録画を更新した後、解釈を再起動してください", + "run_finished": "実行完了", + "run_failed": "実行の開始に失敗しました" + }, + "modal": { + "use_previous": "この操作の条件として前回の選択を使用しますか?", + "previous_action": "前回の操作: ", + "element_text": "テキスト要素 " + } + }, + "recording_page": { + "loader": { + "browser_startup": "ブラウザを起動中...{{url}}に移動中" + } + }, + "integration_settings": { + "title": "Google Sheetと連携", + "descriptions": { + "sync_info": "このオプションを有効にすると、このロボットがタスクを正常に実行するたびに、取得したデータがGoogle Sheetに追加されます。", + "authenticated_as": "認証済みユーザー: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheetの連携が完了しました。", + "content": "このロボットが正常に実行を完了するたびに、取得したデータはGoogle Sheet {{sheetName}}に追加されます。データの更新は", + "here": "こちら", + "note": "注意:", + "sync_limitation": "Google Sheetsとの連携前に抽出されたデータは同期されません。連携後に抽出されたデータのみが同期されます。" + } + }, + "buttons": { + "authenticate": "Googleで認証", + "fetch_sheets": "Google Sheetsを取得", + "remove_integration": "連携を解除", + "submit": "送信" + }, + "fields": { + "select_sheet": "Google Sheetを選択", + "selected_sheet": "選択したシート: {{name}} (ID: {{id}})" + } + }, + "robot_duplication": { + "title": "ロボットを複製", + "descriptions": { + "purpose": "ロボットの複製は、同じ構造のページからデータを抽出する際に便利です。", + "example": "例:{{url1}}用のロボットを作成した場合、ロボットを一から作り直すことなく、{{url2}}のような類似のページをスクレイピングするために複製できます。", + "warning": "⚠️ 新しいページが元のページと同じ構造であることを確認してください。" + }, + "fields": { + "target_url": "ロボットのターゲットURL" + }, + "buttons": { + "duplicate": "ロボットを複製", + "cancel": "キャンセル" + }, + "notifications": { + "robot_not_found": "ロボットの詳細が見つかりません。もう一度お試しください。", + "url_required": "ターゲットURLが必要です。", + "duplicate_success": "ロボットが正常に複製されました。", + "duplicate_error": "ターゲットURLの更新に失敗しました。もう一度お試しください。", + "unknown_error": "ターゲットURLの更新中にエラーが発生しました。" + } + }, + "robot_settings": { + "title": "ロボット設定", + "target_url": "ロボットのターゲットURL", + "robot_id": "ロボットID", + "robot_limit": "ロボットの制限", + "created_by_user": "作成したユーザー", + "created_at": "作成日時", + "errors": { + "robot_not_found": "ロボットの詳細が見つかりませんでした。もう一度試してください。" + } + }, + "robot_edit": { + "title": "ロボットを編集", + "change_name": "ロボット名の変更", + "robot_limit": "ロボットの制限", + "save": "変更を保存", + "cancel": "キャンセル", + "notifications": { + "update_success": "ロボットが正常に更新されました。", + "update_failed": "ロボットの更新に失敗しました。もう一度試してください。", + "update_error": "ロボットの更新中にエラーが発生しました。" + } + }, + "schedule_settings": { + "title": "スケジュール設定", + "run_every": "実行間隔", + "start_from": "開始日", + "on_day": "日付", + "at_around": "時刻", + "timezone": "タイムゾーン", + "buttons": { + "delete_schedule": "スケジュールを削除", + "save_schedule": "スケジュールを保存", + "cancel": "キャンセル" + }, + "labels": { + "in_between": "間隔", + "run_once_every": "実行間隔", + "start_from_label": "開始日", + "on_day_of_month": "月の日付", + "on_day": { + "st": "日", + "nd": "日", + "rd": "日", + "th": "日" + } + } + }, + "main_page": { + "notifications": { + "interpretation_success": "ロボット{{name}}の解釈に成功しました", + "interpretation_failed": "ロボット{{name}}の解釈に失敗しました", + "run_started": "ロボット{{name}}を実行中", + "run_start_failed": "ロボット{{name}}の実行に失敗しました", + "schedule_success": "ロボット{{name}}のスケジュールが正常に設定されました", + "schedule_failed": "ロボット{{name}}のスケジュール設定に失敗しました", + "abort_success": "ロボット{{name}}の解釈を中止しました", + "abort_failed": "ロボット{{name}}の解釈中止に失敗しました" + }, + "menu": { + "recordings": "ロボット", + "runs": "実行", + "proxy": "プロキシ", + "apikey": "APIキー" + } + }, + "browser_window": { + "attribute_modal": { + "title": "属性を選択", + "notifications": { + "list_select_success": "リストが正常に選択されました。抽出するテキストデータを選択してください。", + "pagination_select_success": "ページネーション要素が正常に選択されました。" + } + }, + "attribute_options": { + "anchor": { + "text": "テキスト: {{text}}", + "url": "URL: {{url}}" + }, + "image": { + "alt_text": "代替テキスト: {{altText}}", + "image_url": "画像URL: {{imageUrl}}" + }, + "default": { + "text": "テキスト: {{text}}" + } + } + }, + "runs_table": { + "run_type_chips": { + "manual_run": "手動実行", + "scheduled_run": "スケジュール実行", + "api": "API", + "unknown_run_type": "不明な実行タイプ" + }, + "run_status_chips": { + "success": "成功", + "running": "実行中", + "scheduled": "スケジュール済み", + "failed": "失敗" + }, + "run_settings_modal": { + "title": "実行設定", + "labels": { + "run_id": "実行ID", + "run_by_user": "ユーザーによる実行", + "run_by_schedule": "スケジュールによる実行", + "run_by_api": "APIによる実行", + "run_type": "実行タイプ" + } + } + }, + "run_content": { + "tabs": { + "output_data": "出力データ", + "log": "ログ" + }, + "empty_output": "出力は空です。", + "captured_data": { + "title": "キャプチャされたデータ", + "download_json": "JSONとしてダウンロード", + "download_csv": "CSVとしてダウンロード" + }, + "captured_screenshot": { + "title": "キャプチャされたスクリーンショット", + "download": "スクリーンショットをダウンロード", + "render_failed": "画像のレンダリングに失敗しました" + }, + "buttons": { + "stop": "停止" + } + }, + "navbar": { + "project_name": "Maxun", + "upgrade": { + "button": "アップグレード", + "modal": { + "up_to_date": "最新版です!", + "new_version_available": "新しいバージョンが利用可能です: {{version}}。バグ修正、機能強化のために最新版にアップグレードしてください。", + "view_updates": "すべての更新を", + "view_updates_link": "こちら", + "tabs": { + "manual_setup": "手動セットアップ", + "docker_setup": "Docker Composeセットアップ" + } + } + }, + "menu_items": { + "logout": "ログアウト", + "discord": "Discord", + "youtube": "YouTube", + "twitter": "Twitter (X)", + "language": "言語" + }, + "recording": { + "discard": "破棄" + } + }, + "language_menu": { + "en": "英語", + "es": "スペイン語", + "ja": "日本語", + "zh": "中国語", + "de": "ドイツ語" + } +} diff --git a/public/locales/zh.json b/public/locales/zh.json new file mode 100644 index 00000000..600fd821 --- /dev/null +++ b/public/locales/zh.json @@ -0,0 +1,487 @@ +{ + "login": { + "title": "欢迎回来!", + "email": "电子邮箱", + "password": "密码", + "button": "登录", + "loading": "加载中", + "register_prompt": "还没有账号?", + "register_link": "注册", + "welcome_notification": "欢迎使用 Maxun!", + "error_notification": "登录失败。请重试。" + }, + "register": { + "title": "注册账号", + "email": "电子邮箱", + "password": "密码", + "button": "注册", + "loading": "加载中", + "register_prompt": "已有账号?", + "login_link": "登录", + "welcome_notification": "欢迎使用 Maxun!", + "error_notification": "注册失败。请重试。" + }, + "recordingtable": { + "run": "运行", + "name": "名称", + "schedule": "计划", + "integrate": "集成", + "settings": "设置", + "options": "选项", + "heading": "我的机器人", + "new": "创建机器人", + "modal": { + "title": "输入URL", + "label": "URL", + "button": "开始录制" + }, + "edit": "编辑", + "delete": "删除", + "duplicate": "复制", + "search": "搜索机器人...", + "notifications": { + "delete_warning": "无法删除机器人,因为它有关联的运行记录", + "delete_success": "机器人删除成功" + } + }, + "mainmenu": { + "recordings": "机器人", + "runs": "运行记录", + "proxy": "代理", + "apikey": "API密钥", + "feedback": "加入 Maxun Cloud", + "apidocs": "网站转API" + }, + "runstable": { + "runs": "所有运行记录", + "runStatus": "状态", + "runName": "名称", + "startedAt": "开始时间", + "finishedAt": "结束时间", + "delete": "删除", + "settings": "设置", + "search": "搜索运行记录...", + "notifications": { + "no_runs": "未找到运行记录。请重试。", + "delete_success": "运行记录删除成功" + } + }, + "proxy": { + "title": "代理设置", + "tab_standard": "标准代理", + "tab_rotation": "自动代理轮换", + "server_url": "代理服务器URL", + "server_url_helper": "用于所有机器人的代理。支持HTTP和SOCKS代理。示例 http://myproxy.com:3128 或 socks5://myproxy.com:3128。简短形式 myproxy.com:3128 被视为HTTP代理。", + "requires_auth": "需要认证?", + "username": "用户名", + "password": "密码", + "add_proxy": "添加代理", + "test_proxy": "测试代理", + "remove_proxy": "删除代理", + "table": { + "proxy_url": "代理URL", + "requires_auth": "需要认证" + }, + "coming_soon": "即将推出 - 开源版(基础轮换)和云版(高级轮换)。如果您不想管理基础设施,请加入我们的云服务等候名单以获得早期访问权限。", + "join_waitlist": "加入Maxun Cloud等候名单", + "alert": { + "title": "如果您的代理需要用户名和密码,请务必将它们与代理URL分开提供。", + "right_way": "正确方式", + "wrong_way": "错误方式", + "proxy_url": "代理URL:", + "username": "用户名:", + "password": "密码:" + }, + "notifications": { + "config_success": "代理配置提交成功", + "config_error": "提交代理配置失败。请重试。", + "test_success": "代理配置运行正常", + "test_error": "测试代理配置失败。请重试。", + "fetch_success": "成功获取代理配置", + "remove_success": "成功删除代理配置", + "remove_error": "删除代理配置失败。请重试。" + } + }, + "apikey": { + "title": "管理API密钥", + "default_name": "Maxun API密钥", + "table": { + "name": "API密钥名称", + "key": "API密钥", + "actions": "操作" + }, + "actions": { + "copy": "复制", + "show": "显示", + "hide": "隐藏", + "delete": "删除" + }, + "no_key_message": "您还未生成API密钥。", + "generate_button": "生成API密钥", + "notifications": { + "fetch_error": "获取API密钥失败 - ${error}", + "generate_success": "API密钥生成成功", + "generate_error": "生成API密钥失败 - ${error}", + "delete_success": "API密钥删除成功", + "delete_error": "删除API密钥失败 - ${error}", + "copy_success": "API密钥复制成功" + } + }, + "action_description": { + "text": { + "title": "捕获文本", + "description": "将鼠标悬停在要提取的文本上并点击选择" + }, + "screenshot": { + "title": "捕获截图", + "description": "捕获当前页面的部分或全部截图。" + }, + "list": { + "title": "捕获列表", + "description": "将鼠标悬停在要提取的列表上。选择后,您可以将鼠标悬停在所选列表中的所有文本上。点击选择它们。" + }, + "default": { + "title": "您想提取什么数据?", + "description": "机器人设计为一次执行一个操作。您可以选择以下任何选项。" + }, + "list_stages": { + "initial": "选择要提取的列表及其中的文本", + "pagination": "选择机器人如何捕获列表的其余部分", + "limit": "选择要提取的项目数量", + "complete": "捕获完成" + } + }, + "right_panel": { + "buttons": { + "capture_list": "捕获列表", + "capture_text": "捕获文本", + "capture_screenshot": "捕获截图", + "confirm": "确认", + "discard": "放弃", + "confirm_capture": "确认捕获", + "confirm_pagination": "确认分页", + "confirm_limit": "确认限制", + "finish_capture": "完成捕获", + "finish": "完成", + "cancel": "取消" + }, + "screenshot": { + "capture_fullpage": "捕获整页", + "capture_visible": "捕获可见部分", + "display_fullpage": "获取整页截图", + "display_visible": "获取可见部分截图" + }, + "pagination": { + "title": "如何在页面上找到下一个列表项?", + "click_next": "点击下一页导航到下一页", + "click_load_more": "点击加载更多来加载更多项目", + "scroll_down": "向下滚动加载更多项目", + "scroll_up": "向上滚动加载更多项目", + "none": "没有更多项目可加载" + }, + "limit": { + "title": "您想要提取的最大行数是多少?", + "custom": "自定义", + "enter_number": "输入数字" + }, + "fields": { + "label": "标签", + "data": "数据", + "field_label": "字段标签", + "field_data": "字段数据" + }, + "messages": { + "list_selected": "列表选择成功" + }, + "errors": { + "select_pagination": "请选择分页类型。", + "select_pagination_element": "请先选择分页元素。", + "select_limit": "请选择限制或输入自定义限制。", + "invalid_limit": "请输入有效的限制。", + "confirm_text_fields": "请确认所有文本字段", + "unable_create_settings": "无法创建列表设置。请确保您已为列表定义了字段。", + "capture_text_discarded": "文本捕获已放弃", + "capture_list_discarded": "列表捕获已放弃" + } + }, + "save_recording": { + "title": "保存机器人", + "robot_name": "机器人名称", + "buttons": { + "save": "保存", + "confirm": "确认" + }, + "notifications": { + "save_success": "机器人保存成功" + }, + "errors": { + "user_not_logged": "用户未登录。无法保存录制。", + "exists_warning": "已存在同名机器人,请确认是否覆盖机器人。" + }, + "tooltips": { + "saving": "正在优化并保存工作流程" + } + }, + "browser_recording": { + "modal": { + "confirm_discard": "您确定要放弃录制吗?" + }, + "notifications": { + "terminated": "当前录制已终止" + } + }, + "interpretation_log": { + "titles": { + "output_preview": "输出数据预览", + "screenshot": "截图" + }, + "messages": { + "additional_rows": "完成录制后将提取更多数据行。", + "successful_training": "您已成功训练机器人执行操作!点击下方按钮预览机器人将提取的数据。", + "no_selection": "看起来您还没有选择要提取的内容。选择后,机器人将在此处显示您的选择预览。" + }, + "data_sections": { + "binary_received": "---------- 已接收二进制输出数据 ----------", + "serializable_received": "---------- 已接收可序列化输出数据 ----------", + "mimetype": "MIME类型:", + "image_below": "图片显示如下:", + "separator": "--------------------------------------------------" + } + }, + "interpretation_buttons": { + "buttons": { + "preview": "获取输出数据预览", + "yes": "是", + "no": "否" + }, + "messages": { + "extracting": "正在提取数据...请等待10秒到1分钟", + "restart_required": "更新录制后请重新启动解释", + "run_finished": "运行完成", + "run_failed": "运行启动失败" + }, + "modal": { + "use_previous": "您要将之前的选择用作执行此操作的条件吗?", + "previous_action": "您之前的操作是:", + "element_text": "在文本元素上 " + } + }, + "recording_page": { + "loader": { + "browser_startup": "正在启动浏览器...正在导航至{{url}}" + } + }, + "integration_settings": { + "title": "与Google Sheet集成", + "descriptions": { + "sync_info": "如果启用此选项,每次机器人成功运行任务时,捕获的数据都会追加到您的Google Sheet中。", + "authenticated_as": "已验证身份: {{email}}" + }, + "alerts": { + "success": { + "title": "Google Sheet集成成功。", + "content": "每次此机器人创建成功运行时,捕获的数据都会追加到您的Google Sheet {{sheetName}}中。您可以查看数据更新", + "here": "在此处", + "note": "注意:", + "sync_limitation": "与Google Sheets集成之前提取的数据将不会同步到Google Sheet中。只有集成后提取的数据才会同步。" + } + }, + "buttons": { + "authenticate": "使用Google验证", + "fetch_sheets": "获取Google Sheets", + "remove_integration": "移除集成", + "submit": "提交" + }, + "fields": { + "select_sheet": "选择Google Sheet", + "selected_sheet": "已选择表格: {{name}} (ID: {{id}})" + } + }, + "robot_duplication": { + "title": "复制机器人", + "descriptions": { + "purpose": "机器人复制功能用于从具有相同结构的页面提取数据。", + "example": "示例:如果您已经为{{url1}}创建了机器人,您可以复制它来抓取类似的页面(如{{url2}}),而无需从头开始训练机器人。", + "warning": "⚠️ 确保新页面与原始页面具有相同的结构。" + }, + "fields": { + "target_url": "机器人目标URL" + }, + "buttons": { + "duplicate": "复制机器人", + "cancel": "取消" + }, + "notifications": { + "robot_not_found": "找不到机器人详细信息。请重试。", + "url_required": "需要目标URL。", + "duplicate_success": "机器人复制成功。", + "duplicate_error": "更新目标URL失败。请重试。", + "unknown_error": "更新目标URL时发生错误。" + } + }, + "robot_settings": { + "title": "机器人设置", + "target_url": "机器人目标URL", + "robot_id": "机器人ID", + "robot_limit": "机器人限制", + "created_by_user": "由用户创建", + "created_at": "机器人创建时间", + "errors": { + "robot_not_found": "无法找到机器人详细信息。请重试。" + } + }, + "robot_edit": { + "title": "编辑机器人", + "change_name": "更改机器人名称", + "robot_limit": "机器人限制", + "save": "保存更改", + "cancel": "取消", + "notifications": { + "update_success": "机器人更新成功。", + "update_failed": "无法更新机器人。请重试。", + "update_error": "更新机器人时发生错误。" + } + }, + "schedule_settings": { + "title": "计划设置", + "run_every": "每次运行", + "start_from": "开始于", + "on_day": "在日", + "at_around": "大约在", + "timezone": "时区", + "buttons": { + "delete_schedule": "删除计划", + "save_schedule": "保存计划", + "cancel": "取消" + }, + "labels": { + "in_between": "之间", + "run_once_every": "每次运行", + "start_from_label": "开始于", + "on_day_of_month": "月份日期", + "on_day": { + "st": "日", + "nd": "日", + "rd": "日", + "th": "日" + } + } + }, + "main_page": { + "notifications": { + "interpretation_success": "机器人{{name}}解释成功", + "interpretation_failed": "机器人{{name}}解释失败", + "run_started": "正在运行机器人:{{name}}", + "run_start_failed": "机器人运行失败:{{name}}", + "schedule_success": "机器人{{name}}调度成功", + "schedule_failed": "机器人{{name}}调度失败", + "abort_success": "成功中止机器人{{name}}的解释", + "abort_failed": "中止机器人{{name}}的解释失败" + }, + "menu": { + "recordings": "机器人", + "runs": "运行", + "proxy": "代理", + "apikey": "API密钥" + } + }, + "browser_window": { + "attribute_modal": { + "title": "选择属性", + "notifications": { + "list_select_success": "列表选择成功。选择要提取的文本数据。", + "pagination_select_success": "分页元素选择成功。" + } + }, + "attribute_options": { + "anchor": { + "text": "文本: {{text}}", + "url": "URL: {{url}}" + }, + "image": { + "alt_text": "替代文本: {{altText}}", + "image_url": "图像URL: {{imageUrl}}" + }, + "default": { + "text": "文本: {{text}}" + } + } + }, + "runs_table": { + "run_type_chips": { + "manual_run": "手动运行", + "scheduled_run": "计划运行", + "api": "API", + "unknown_run_type": "未知运行类型" + }, + "run_status_chips": { + "success": "成功", + "running": "运行中", + "scheduled": "已计划", + "failed": "失败" + }, + "run_settings_modal": { + "title": "运行设置", + "labels": { + "run_id": "运行ID", + "run_by_user": "由用户运行", + "run_by_schedule": "按计划ID运行", + "run_by_api": "由API运行", + "run_type": "运行类型" + } + } + }, + "run_content": { + "tabs": { + "output_data": "输出数据", + "log": "日志" + }, + "empty_output": "输出为空。", + "captured_data": { + "title": "捕获的数据", + "download_json": "下载为JSON", + "download_csv": "下载为CSV" + }, + "captured_screenshot": { + "title": "捕获的截图", + "download": "下载截图", + "render_failed": "图像渲染失败" + }, + "buttons": { + "stop": "停止" + } + }, + "navbar": { + "project_name": "Maxun", + "upgrade": { + "button": "升级", + "modal": { + "up_to_date": "🎉 您已是最新版本!", + "new_version_available": "新版本已可用:{{version}}。升级到最新版本以获取错误修复、增强和新功能!", + "view_updates": "查看所有新更新", + "view_updates_link": "此处", + "tabs": { + "manual_setup": "手动设置升级", + "docker_setup": "Docker Compose设置升级" + } + } + }, + "menu_items": { + "logout": "退出登录", + "discord": "Discord", + "youtube": "YouTube", + "twitter": "Twitter (X)", + "language": "语言" + }, + "recording": { + "discard": "丢弃" + } + }, + "language_menu": { + "en": "英语", + "es": "西班牙语", + "ja": "日语", + "zh": "中文", + "de": "德语" + } +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index c37de9ea..02dff134 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,10 @@ -import React from 'react'; -import { Routes, Route } from 'react-router-dom'; +import React from "react"; +import { Routes, Route } from "react-router-dom"; import { ThemeProvider, createTheme } from "@mui/material/styles"; import { GlobalInfoProvider } from "./context/globalInfo"; import { PageWrapper } from "./pages/PageWrappper"; +import i18n from "./i18n"; + const theme = createTheme({ palette: { @@ -20,14 +22,14 @@ const theme = createTheme({ }, containedPrimary: { // Styles for 'contained' variant with 'primary' color - '&:hover': { + "&:hover": { backgroundColor: "#ff66d9", }, }, outlined: { // Apply white background for all 'outlined' variant buttons backgroundColor: "#ffffff", - '&:hover': { + "&:hover": { backgroundColor: "#f0f0f0", // Optional lighter background on hover }, }, @@ -36,7 +38,7 @@ const theme = createTheme({ MuiLink: { styleOverrides: { root: { - '&:hover': { + "&:hover": { color: "#ff00c3", }, }, @@ -63,7 +65,7 @@ const theme = createTheme({ standardInfo: { backgroundColor: "#fce1f4", color: "#ff00c3", - '& .MuiAlert-icon': { + "& .MuiAlert-icon": { color: "#ff00c3", }, }, @@ -72,7 +74,7 @@ const theme = createTheme({ MuiAlertTitle: { styleOverrides: { root: { - '& .MuiAlert-icon': { + "& .MuiAlert-icon": { color: "#ffffff", }, }, @@ -81,15 +83,16 @@ const theme = createTheme({ }, }); - function App() { return ( - - - } /> - - + + + + } /> + + + ); } diff --git a/src/components/molecules/ActionDescriptionBox.tsx b/src/components/molecules/ActionDescriptionBox.tsx index cad962c7..190c5838 100644 --- a/src/components/molecules/ActionDescriptionBox.tsx +++ b/src/components/molecules/ActionDescriptionBox.tsx @@ -3,6 +3,7 @@ import styled from 'styled-components'; import { Typography, FormControlLabel, Checkbox, Box } from '@mui/material'; import { useActionContext } from '../../context/browserActions'; import MaxunLogo from "../../assets/maxunlogo.png"; +import { useTranslation } from 'react-i18next'; const CustomBoxContainer = styled.div` position: relative; @@ -44,6 +45,7 @@ const Content = styled.div` `; const ActionDescriptionBox = () => { + const { t } = useTranslation(); const { getText, getScreenshot, getList, captureStage } = useActionContext() as { getText: boolean; getScreenshot: boolean; @@ -52,36 +54,36 @@ const ActionDescriptionBox = () => { }; const messages = [ - { stage: 'initial' as const, text: 'Select the list you want to extract along with the texts inside it' }, - { stage: 'pagination' as const, text: 'Select how the robot can capture the rest of the list' }, - { stage: 'limit' as const, text: 'Choose the number of items to extract' }, - { stage: 'complete' as const, text: 'Capture is complete' }, + { stage: 'initial' as const, text: t('action_description.list_stages.initial') }, + { stage: 'pagination' as const, text: t('action_description.list_stages.pagination') }, + { stage: 'limit' as const, text: t('action_description.list_stages.limit') }, + { stage: 'complete' as const, text: t('action_description.list_stages.complete') }, ]; - const stages = messages.map(({ stage }) => stage); // Create a list of stages - const currentStageIndex = stages.indexOf(captureStage); // Get the index of the current stage + const stages = messages.map(({ stage }) => stage); + const currentStageIndex = stages.indexOf(captureStage); const renderActionDescription = () => { if (getText) { return ( <> - Capture Text - Hover over the texts you want to extract and click to select them + {t('action_description.text.title')} + {t('action_description.text.description')} ); } else if (getScreenshot) { return ( <> - Capture Screenshot - Capture a partial or full page screenshot of the current page. + {t('action_description.screenshot.title')} + {t('action_description.screenshot.description')} ); } else if (getList) { return ( <> - Capture List + {t('action_description.list.title')} - Hover over the list you want to extract. Once selected, you can hover over all texts inside the list you selected. Click to select them. + {t('action_description.list.description')} {messages.map(({ stage, text }, index) => ( @@ -89,7 +91,7 @@ const ActionDescriptionBox = () => { key={stage} control={ } @@ -102,8 +104,8 @@ const ActionDescriptionBox = () => { } else { return ( <> - What data do you want to extract? - A robot is designed to perform one action at a time. You can choose any of the options below. + {t('action_description.default.title')} + {t('action_description.default.description')} ); } @@ -111,7 +113,7 @@ const ActionDescriptionBox = () => { return ( - + {renderActionDescription()} @@ -120,4 +122,4 @@ const ActionDescriptionBox = () => { ); }; -export default ActionDescriptionBox; +export default ActionDescriptionBox; \ No newline at end of file diff --git a/src/components/molecules/BrowserRecordingSave.tsx b/src/components/molecules/BrowserRecordingSave.tsx index 03758717..e1eff20e 100644 --- a/src/components/molecules/BrowserRecordingSave.tsx +++ b/src/components/molecules/BrowserRecordingSave.tsx @@ -5,8 +5,10 @@ import { useGlobalInfoStore } from '../../context/globalInfo'; import { stopRecording } from "../../api/recording"; import { useNavigate } from 'react-router-dom'; import { GenericModal } from "../atoms/GenericModal"; +import { useTranslation } from 'react-i18next'; const BrowserRecordingSave = () => { + const { t } = useTranslation(); const [openModal, setOpenModal] = useState(false); const { recordingName, browserId, setBrowserId, notify } = useGlobalInfoStore(); const navigate = useNavigate(); @@ -14,7 +16,7 @@ const BrowserRecordingSave = () => { const goToMainMenu = async () => { if (browserId) { await stopRecording(browserId); - notify('warning', 'Current Recording was terminated'); + notify('warning', t('browser_recording.notifications.terminated')); setBrowserId(null); } navigate('/'); @@ -25,30 +27,29 @@ const BrowserRecordingSave = () => {
setOpenModal(false)} modalStyle={modalStyle}> - Are you sure you want to discard the recording? + {t('browser_recording.modal.confirm_discard')} @@ -60,7 +61,7 @@ const BrowserRecordingSave = () => { ); } -export default BrowserRecordingSave +export default BrowserRecordingSave; const modalStyle = { top: '25%', diff --git a/src/components/molecules/ColapsibleRow.tsx b/src/components/molecules/ColapsibleRow.tsx index 95b144d7..5e4be9da 100644 --- a/src/components/molecules/ColapsibleRow.tsx +++ b/src/components/molecules/ColapsibleRow.tsx @@ -10,6 +10,7 @@ import { RunContent } from "./RunContent"; import { GenericModal } from "../atoms/GenericModal"; import { modalStyle } from "./AddWhereCondModal"; import { getUserById } from "../../api/auth"; +import { useTranslation } from "react-i18next"; interface RunTypeChipProps { runByUserId?: string; @@ -18,10 +19,12 @@ interface RunTypeChipProps { } const RunTypeChip: React.FC = ({ runByUserId, runByScheduledId, runByAPI }) => { - if (runByUserId) return ; - if (runByScheduledId) return ; - if (runByAPI) return ; - return ; + const { t } = useTranslation(); + + if (runByUserId) return ; + if (runByScheduledId) return ; + if (runByAPI) return ; + return ; }; interface CollapsibleRowProps { @@ -33,6 +36,7 @@ interface CollapsibleRowProps { runningRecordingName: string; } export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRunHandler, runningRecordingName }: CollapsibleRowProps) => { + const { t } = useTranslation(); const [open, setOpen] = useState(isOpen); const [openSettingsModal, setOpenSettingsModal] = useState(false); const [userEmail, setUserEmail] = useState(null); @@ -99,12 +103,12 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun } else { switch (column.id) { case 'runStatus': - return ( + return ( - {row.status === 'success' && } - {row.status === 'running' && } - {row.status === 'scheduled' && } - {row.status === 'failed' && } + {row.status === 'success' && } + {row.status === 'running' && } + {row.status === 'scheduled' && } + {row.status === 'failed' && } ) case 'delete': @@ -133,21 +137,35 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, currentLog, abortRun modalStyle={modalStyle} > <> - Run Settings + + {t('runs_table.run_settings_modal.title')} + - Run Type: - + + {t('runs_table.run_settings_modal.labels.run_type')}: + + diff --git a/src/components/molecules/IntegrationSettings.tsx b/src/components/molecules/IntegrationSettings.tsx index b34bc0e9..c4b13e8c 100644 --- a/src/components/molecules/IntegrationSettings.tsx +++ b/src/components/molecules/IntegrationSettings.tsx @@ -15,6 +15,7 @@ import { useGlobalInfoStore } from "../../context/globalInfo"; import { getStoredRecording } from "../../api/storage"; import { apiUrl } from "../../apiConfig.js"; import Cookies from 'js-cookie'; +import { useTranslation } from "react-i18next"; interface IntegrationProps { isOpen: boolean; @@ -33,6 +34,7 @@ export const IntegrationSettingsModal = ({ handleStart, handleClose, }: IntegrationProps) => { + const { t } = useTranslation(); const [settings, setSettings] = useState({ spreadsheetId: "", spreadsheetName: "", @@ -77,8 +79,9 @@ export const IntegrationSettingsModal = ({ ); notify( "error", - `Error fetching spreadsheet files: ${error.response?.data?.message || error.message - }` + t('integration_settings.errors.fetch_error', { + message: error.response?.data?.message || error.message + }) ); } }; @@ -107,7 +110,7 @@ export const IntegrationSettingsModal = ({ }, { withCredentials: true } ); - notify(`success`, `Google Sheet selected successfully`) + notify(`success`, t('integration_settings.notifications.sheet_selected')); console.log("Google Sheet ID updated:", response.data); } catch (error: any) { console.error( @@ -168,38 +171,28 @@ export const IntegrationSettingsModal = ({ return ( -
+ }}> - Integrate with Google Sheet{" "} - {/* */} + {t('integration_settings.title')} {recording && recording.google_sheet_id ? ( <> - Google Sheet Integrated Successfully. - Every time this robot creates a successful run, its captured data - is appended to your {recording.google_sheet_name} Google Sheet. - You can check the data updates{" "} - {t('integration_settings.alerts.success.title')} + {t('integration_settings.alerts.success.content', { sheetName: recording.google_sheet_name })} + - here - - . + rel="noreferrer"> + {t('integration_settings.alerts.success.here')} + .
- Note: The data extracted before integrating with - Google Sheets will not be synced in the Google Sheet. Only the - data extracted after the integration will be synced. + {t('integration_settings.alerts.success.note')} {t('integration_settings.alerts.success.sync_limitation')}
) : ( <> {!recording?.google_sheet_email ? ( <> -

- If you enable this option, every time this robot runs a task - successfully, its captured data will be appended to your - Google Sheet. -

+

{t('integration_settings.descriptions.sync_info')}

) : ( <> {recording.google_sheet_email && ( - Authenticated as: {recording.google_sheet_email} + {t('integration_settings.descriptions.authenticated_as', { + email: recording.google_sheet_email + })} )} @@ -247,14 +238,14 @@ export const IntegrationSettingsModal = ({ color="primary" onClick={fetchSpreadsheetFiles} > - Fetch Google Spreadsheets + {t('integration_settings.buttons.fetch_sheets')}
@@ -263,7 +254,7 @@ export const IntegrationSettingsModal = ({ - Selected Sheet:{" "} - { - spreadsheets.find( - (s) => s.id === settings.spreadsheetId - )?.name - }{" "} - (ID: {settings.spreadsheetId}) + {t('integration_settings.fields.selected_sheet', { + name: spreadsheets.find((s) => s.id === settings.spreadsheetId)?.name, + id: settings.spreadsheetId + })} )} @@ -298,7 +286,7 @@ export const IntegrationSettingsModal = ({ style={{ marginTop: "10px" }} disabled={!settings.spreadsheetId || loading} > - Submit + {t('integration_settings.buttons.submit')} )} diff --git a/src/components/molecules/InterpretationButtons.tsx b/src/components/molecules/InterpretationButtons.tsx index 9d983761..624a57b4 100644 --- a/src/components/molecules/InterpretationButtons.tsx +++ b/src/components/molecules/InterpretationButtons.tsx @@ -7,6 +7,7 @@ import { useGlobalInfoStore } from "../../context/globalInfo"; import { GenericModal } from "../atoms/GenericModal"; import { WhereWhatPair } from "maxun-core"; import HelpIcon from '@mui/icons-material/Help'; +import { useTranslation } from "react-i18next"; interface InterpretationButtonsProps { enableStepping: (isPaused: boolean) => void; @@ -23,6 +24,7 @@ const interpretationInfo: InterpretationInfo = { }; export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsProps) => { + const { t } = useTranslation(); const [info, setInfo] = useState(interpretationInfo); const [decisionModal, setDecisionModal] = useState<{ pair: WhereWhatPair | null, @@ -44,9 +46,9 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP const breakpointHitHandler = useCallback(() => { setInfo({ running: false, isPaused: true }); - notify('warning', 'Please restart the interpretation after updating the recording'); + notify('warning', t('interpretation_buttons.messages.restart_required')); enableStepping(true); - }, [enableStepping]); + }, [enableStepping, t]); const decisionHandler = useCallback( ({ pair, actionType, lastData }: { pair: WhereWhatPair | null, actionType: string, lastData: { selector: string, action: string, tagName: string, innerText: string } }) => { @@ -73,11 +75,12 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP return ( <> - Do you want to use your previous selection as a condition for performing this action? + {t('interpretation_buttons.modal.use_previous')} - Your previous action was: {decisionModal.action}, on an element with text {decisionModal.innerText} + {t('interpretation_buttons.modal.previous_action')} {decisionModal.action}, + {t('interpretation_buttons.modal.element_text')} {decisionModal.innerText} @@ -105,9 +108,9 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP const finished = await interpretCurrentRecording(); setInfo({ ...info, running: false }); if (finished) { - notify('info', 'Run finished'); + notify('info', t('interpretation_buttons.messages.run_finished')); } else { - notify('error', 'Run failed to start'); + notify('error', t('interpretation_buttons.messages.run_failed')); } } }; @@ -139,9 +142,12 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP disabled={info.running} sx={{ display: 'grid' }} > - {info.running ? - Extracting data...please wait for 10secs to 1min - : 'Get Preview of Output Data'} + {info.running ? ( + + + {t('interpretation_buttons.messages.extracting')} + + ) : t('interpretation_buttons.buttons.preview')} { }} @@ -166,8 +172,12 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP {handleDescription()}
- - + +
diff --git a/src/components/molecules/InterpretationLog.tsx b/src/components/molecules/InterpretationLog.tsx index 24ba848b..0a771535 100644 --- a/src/components/molecules/InterpretationLog.tsx +++ b/src/components/molecules/InterpretationLog.tsx @@ -17,6 +17,7 @@ import StorageIcon from '@mui/icons-material/Storage'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import { SidePanelHeader } from './SidePanelHeader'; import { useGlobalInfoStore } from '../../context/globalInfo'; +import { useTranslation } from 'react-i18next'; interface InterpretationLogProps { isOpen: boolean; @@ -24,6 +25,7 @@ interface InterpretationLogProps { } export const InterpretationLog: React.FC = ({ isOpen, setIsOpen }) => { + const { t } = useTranslation(); const [log, setLog] = useState(''); const [customValue, setCustomValue] = useState(''); const [tableData, setTableData] = useState([]); @@ -63,28 +65,29 @@ export const InterpretationLog: React.FC = ({ isOpen, se const handleSerializableCallback = useCallback((data: any) => { setLog((prevState) => - prevState + '\n' + '---------- Serializable output data received ----------' + '\n' - + JSON.stringify(data, null, 2) + '\n' + '--------------------------------------------------'); + prevState + '\n' + t('interpretation_log.data_sections.serializable_received') + '\n' + + JSON.stringify(data, null, 2) + '\n' + t('interpretation_log.data_sections.separator')); if (Array.isArray(data)) { setTableData(data); } scrollLogToBottom(); - }, [log, scrollLogToBottom]); + }, [log, scrollLogToBottom, t]); const handleBinaryCallback = useCallback(({ data, mimetype }: any) => { const base64String = Buffer.from(data).toString('base64'); const imageSrc = `data:${mimetype};base64,${base64String}`; setLog((prevState) => - prevState + '\n' + '---------- Binary output data received ----------' + '\n' - + `mimetype: ${mimetype}` + '\n' + 'Image is rendered below:' + '\n' - + '------------------------------------------------'); + prevState + '\n' + t('interpretation_log.data_sections.binary_received') + '\n' + + t('interpretation_log.data_sections.mimetype') + mimetype + '\n' + + t('interpretation_log.data_sections.image_below') + '\n' + + t('interpretation_log.data_sections.separator')); setBinaryData(imageSrc); scrollLogToBottom(); - }, [log, scrollLogToBottom]); + }, [log, scrollLogToBottom, t]); const handleCustomValueChange = (event: React.ChangeEvent) => { @@ -136,7 +139,8 @@ export const InterpretationLog: React.FC = ({ isOpen, se }, }} > - Output Data Preview + + {t('interpretation_log.titles.output_preview')} = ({ isOpen, se }, }} > - - Output Data Preview - + + + {t('interpretation_log.titles.output_preview')} +
= ({ isOpen, se { binaryData ? (
- Screenshot - Binary Output + + {t('interpretation_log.titles.screenshot')} + + {t('interpretation_log.titles.screenshot')}
) : tableData.length > 0 ? ( <> @@ -193,7 +200,9 @@ export const InterpretationLog: React.FC = ({ isOpen, se - Additional rows of data will be extracted once you finish recording. + + {t('interpretation_log.messages.additional_rows')} + ) : ( @@ -201,13 +210,13 @@ export const InterpretationLog: React.FC = ({ isOpen, se {hasScrapeListAction || hasScrapeSchemaAction || hasScreenshotAction ? ( <> - You've successfully trained the robot to perform actions! Click on the button below to get a preview of the data your robot will extract. + {t('interpretation_log.messages.successful_training')} ) : ( - It looks like you have not selected anything for extraction yet. Once you do, the robot will show a preview of your selections here. + {t('interpretation_log.messages.no_selection')} )} @@ -219,4 +228,4 @@ export const InterpretationLog: React.FC = ({ isOpen, se ); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/components/molecules/NavBar.tsx b/src/components/molecules/NavBar.tsx index 24c41b20..142d45ab 100644 --- a/src/components/molecules/NavBar.tsx +++ b/src/components/molecules/NavBar.tsx @@ -1,10 +1,11 @@ +import { useTranslation } from "react-i18next"; import React, { useState, useContext, useEffect } from 'react'; import axios from 'axios'; import styled from "styled-components"; import { stopRecording } from "../../api/recording"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { IconButton, Menu, MenuItem, Typography, Chip, Button, Modal, Tabs, Tab, Box, Snackbar } from "@mui/material"; -import { AccountCircle, Logout, Clear, YouTube, X, Update, Close } from "@mui/icons-material"; +import { AccountCircle, Logout, Clear, YouTube, X, Update, Close, Language } from "@mui/icons-material"; import { useNavigate } from 'react-router-dom'; import { AuthContext } from '../../context/auth'; import { SaveRecording } from '../molecules/SaveRecording'; @@ -13,18 +14,26 @@ import { apiUrl } from '../../apiConfig'; import MaxunLogo from "../../assets/maxunlogo.png"; import packageJson from "../../../package.json" + interface NavBarProps { recordingName: string; isRecording: boolean; } -export const NavBar: React.FC = ({ recordingName, isRecording }) => { - const { notify, browserId, setBrowserId, recordingUrl } = useGlobalInfoStore(); +export const NavBar: React.FC = ({ + recordingName, + isRecording, +}) => { + const { notify, browserId, setBrowserId } = useGlobalInfoStore(); const { state, dispatch } = useContext(AuthContext); const { user } = state; const navigate = useNavigate(); + const { t, i18n } = useTranslation(); const [anchorEl, setAnchorEl] = useState(null); + + const [langAnchorEl, setLangAnchorEl] = useState(null); + const currentVersion = packageJson.version; const [open, setOpen] = useState(false); @@ -40,7 +49,7 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) => return version; } catch (error) { console.error("Failed to fetch latest version:", error); - return null; // Handle errors gracefully + return null; } }; @@ -62,33 +71,43 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) => setAnchorEl(event.currentTarget); }; + const handleLangMenuOpen = (event: React.MouseEvent) => { + setLangAnchorEl(event.currentTarget); + }; + const handleMenuClose = () => { setAnchorEl(null); + setLangAnchorEl(null); }; const logout = async () => { - dispatch({ type: 'LOGOUT' }); - window.localStorage.removeItem('user'); + dispatch({ type: "LOGOUT" }); + window.localStorage.removeItem("user"); const { data } = await axios.get(`${apiUrl}/auth/logout`); - notify('success', data.message); - navigate('/login'); + notify("success", data.message); + navigate("/login"); }; const goToMainMenu = async () => { if (browserId) { await stopRecording(browserId); - notify('warning', 'Current Recording was terminated'); + notify("warning", t('browser_recording.notifications.terminated')); setBrowserId(null); } - navigate('/'); + navigate("/"); + }; + + const changeLanguage = (lang: string) => { + i18n.changeLanguage(lang); + localStorage.setItem("language", lang); }; useEffect(() => { const checkForUpdates = async () => { const latestVersion = await fetchLatestVersion(); - setLatestVersion(latestVersion); // Set the latest version state + setLatestVersion(latestVersion); if (latestVersion && latestVersion !== currentVersion) { - setIsUpdateAvailable(true); // Show a notification or highlight the "Upgrade" button + setIsUpdateAvailable(true); } }; checkForUpdates(); @@ -101,7 +120,7 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) => open={isUpdateAvailable} onClose={() => setIsUpdateAvailable(false)} message={ - `New version ${latestVersion} available! Click "Upgrade" to update.` + `${t('navbar.upgrade.modal.new_version_available', { version: latestVersion })} ${t('navbar.upgrade.modal.view_updates')}` } action={ <> @@ -118,7 +137,7 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) => borderRadius: '5px', }} > - Upgrade + {t('navbar.upgrade.button')} = ({ recordingName, isRecording }) => } }} /> - )}
= ({ recordingName, isRecording }) => justifyContent: 'flex-start', }}> -
Maxun
+
{t('navbar.project_name')}
= ({ recordingName, isRecording }) => border: "#00000099 1px solid", '&:hover': { color: '#ff00c3', border: '#ff00c3 1px solid' } }}> - Upgrade Maxun + {t('navbar.upgrade.button')} Maxun = ({ recordingName, isRecording }) => Checking for updates... ) : currentVersion === latestVersion ? ( - 🎉 You're up to date! + {t('navbar.upgrade.modal.up_to_date')} ) : ( <> - A new version is available: {latestVersion}. Upgrade to the latest version for bug fixes, enhancements and new features! + {t('navbar.upgrade.modal.new_version_available', { version: latestVersion })}
- View all the new updates + {t('navbar.upgrade.modal.view_updates')} {' '}here.
= ({ recordingName, isRecording }) => sx={{ marginTop: 2, marginBottom: 2 }} centered > - - + + {tab === 0 && ( @@ -285,7 +303,7 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) => PaperProps={{ sx: { width: '180px' } }} > { handleMenuClose(); logout(); }}> - Logout + {t('navbar.menu_items.logout')} { window.open('https://discord.gg/5GbPjBUkws', '_blank'); @@ -302,6 +320,63 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) => }}> Twiiter (X) + + {t('navbar.menu_items.language')} + + + { + changeLanguage("en"); + handleMenuClose(); + }} + > + English + + { + changeLanguage("es"); + handleMenuClose(); + }} + > + Español + + { + changeLanguage("ja"); + handleMenuClose(); + }} + > + 日本語 + + { + changeLanguage("zh"); + handleMenuClose(); + }} + > + 中文 + + { + changeLanguage("de"); + handleMenuClose(); + }} + > + Deutsch + + ) : ( @@ -315,14 +390,80 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) => '&:hover': { color: 'white', backgroundColor: 'red' } }}> - Discard + {t('navbar.recording.discard')} )}
- ) : "" - } + ) : ( + <> + {t("Language")} + + + { + changeLanguage("en"); + handleMenuClose(); + }} + > + English + + { + changeLanguage("es"); + handleMenuClose(); + }} + > + Español + + { + changeLanguage("ja"); + handleMenuClose(); + }} + > + 日本語 + + { + changeLanguage("zh"); + handleMenuClose(); + }} + > + 中文 + + { + changeLanguage("de"); + handleMenuClose(); + }} + > + Deutsch + + + )}
); @@ -331,7 +472,7 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) => const NavBarWrapper = styled.div` grid-area: navbar; background-color: white; - padding:5px; + padding: 5px; display: flex; justify-content: space-between; border-bottom: 1px solid #e0e0e0; diff --git a/src/components/molecules/RecordingsTable.tsx b/src/components/molecules/RecordingsTable.tsx index 651d3677..01bc524b 100644 --- a/src/components/molecules/RecordingsTable.tsx +++ b/src/components/molecules/RecordingsTable.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { useTranslation } from 'react-i18next'; import Paper from '@mui/material/Paper'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; @@ -19,6 +20,7 @@ import { useNavigate } from 'react-router-dom'; import { stopRecording } from "../../api/recording"; import { GenericModal } from '../atoms/GenericModal'; + /** TODO: * 1. allow editing existing robot after persisting browser steps */ @@ -31,30 +33,9 @@ interface Column { format?: (value: string) => string; } -const columns: readonly Column[] = [ - { id: 'interpret', label: 'Run', minWidth: 80 }, - { id: 'name', label: 'Name', minWidth: 80 }, - { - id: 'schedule', - label: 'Schedule', - minWidth: 80, - }, - { - id: 'integrate', - label: 'Integrate', - minWidth: 80, - }, - { - id: 'settings', - label: 'Settings', - minWidth: 80, - }, - { - id: 'options', - label: 'Options', - minWidth: 80, - }, -]; + + + interface Data { id: string; @@ -76,12 +57,38 @@ interface RecordingsTableProps { } export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot }: RecordingsTableProps) => { + const {t} = useTranslation(); const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(10); const [rows, setRows] = React.useState([]); const [isModalOpen, setModalOpen] = React.useState(false); const [searchTerm, setSearchTerm] = React.useState(''); + const columns: readonly Column[] = [ + { id: 'interpret', label: t('recordingtable.run'), minWidth: 80 }, + { id: 'name', label: t('recordingtable.name'), minWidth: 80 }, + { + id: 'schedule', + label: t('recordingtable.schedule'), + minWidth: 80, + }, + { + id: 'integrate', + label: t('recordingtable.integrate'), + minWidth: 80, + }, + { + id: 'settings', + label: t('recordingtable.settings'), + minWidth: 80, + }, + { + id: 'options', + label: t('recordingtable.options'), + minWidth: 80, + }, + ]; + const { notify, setRecordings, browserId, setBrowserId, recordingUrl, setRecordingUrl, recordingName, setRecordingName, recordingId, setRecordingId } = useGlobalInfoStore(); const navigate = useNavigate(); @@ -151,16 +158,17 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl row.name.toLowerCase().includes(searchTerm.toLowerCase()) ); + return ( - My Robots + {t('recordingtable.heading')} - Create Robot + {t('recordingtable.new')}
@@ -253,14 +261,14 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl checkRunsForRecording(row.id).then((result: boolean) => { if (result) { - notify('warning', 'Cannot delete robot as it has associated runs'); + notify('warning', t('recordingtable.notifications.delete_warning')); } }) deleteRecordingFromStorage(row.id).then((result: boolean) => { if (result) { setRows([]); - notify('success', 'Robot deleted successfully'); + notify('success', t('recordingtable.notifications.delete_success')); fetchRecordings(); } }) @@ -297,9 +305,9 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl /> setModalOpen(false)} modalStyle={modalStyle}>
- Enter URL To Extract Data + {t('recordingtable.modal.title')} - Start Training Robot + {t('recordingtable.modal.button')}
@@ -397,6 +405,8 @@ const OptionsButton = ({ handleEdit, handleDelete, handleDuplicate }: OptionsBut setAnchorEl(null); }; + const {t} = useTranslation(); + return ( <> - Edit - - { handleDuplicate(); handleClose(); }}> - - - - Duplicate + {t('recordingtable.edit')} + { handleDelete(); handleClose(); }}> - Delete + {t('recordingtable.delete')} + + { handleDuplicate(); handleClose(); }}> + + + + {t('recordingtable.duplicate')} + + ); diff --git a/src/components/molecules/RobotDuplicate.tsx b/src/components/molecules/RobotDuplicate.tsx index 38b7b422..530aedc6 100644 --- a/src/components/molecules/RobotDuplicate.tsx +++ b/src/components/molecules/RobotDuplicate.tsx @@ -6,6 +6,7 @@ import { useGlobalInfoStore } from '../../context/globalInfo'; import { duplicateRecording, getStoredRecording } from '../../api/storage'; import { WhereWhatPair } from 'maxun-core'; import { getUserById } from "../../api/auth"; +import { useTranslation } from 'react-i18next'; interface RobotMeta { name: string; @@ -54,6 +55,7 @@ interface RobotSettingsProps { } export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => { + const { t } = useTranslation(); const [robot, setRobot] = useState(null); const [targetUrl, setTargetUrl] = useState(''); const { recordingId, notify } = useGlobalInfoStore(); @@ -65,7 +67,6 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia }, [isOpen]); useEffect(() => { - // Update the targetUrl when the robot data is loaded if (robot) { const lastPair = robot?.recording.workflow[robot?.recording.workflow.length - 1]; const url = lastPair?.what.find(action => action.action === "goto")?.args?.[0]; @@ -78,22 +79,17 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia const robot = await getStoredRecording(recordingId); setRobot(robot); } else { - notify('error', 'Could not find robot details. Please try again.'); + notify('error', t('robot_duplication.notifications.robot_not_found')); } } - // const lastPair = robot?.recording.workflow[robot?.recording.workflow.length - 1]; - - // // Find the `goto` action in `what` and retrieve its arguments - // const targetUrl = lastPair?.what.find(action => action.action === "goto")?.args?.[0]; - const handleTargetUrlChange = (e: React.ChangeEvent) => { setTargetUrl(e.target.value); }; const handleSave = async () => { if (!robot || !targetUrl) { - notify('error', 'Target URL is required.'); + notify('error', t('robot_duplication.notifications.url_required')); return; } @@ -103,18 +99,18 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia const success = await duplicateRecording(robot.recording_meta.id, targetUrl); if (success) { - notify('success', 'Robot duplicated successfully.'); - handleStart(robot); // Inform parent about the updated robot + notify('success', t('robot_duplication.notifications.duplicate_success')); + handleStart(robot); handleClose(); setTimeout(() => { window.location.reload(); }, 1000); } else { - notify('error', 'Failed to update the Target URL. Please try again.'); + notify('error', t('robot_duplication.notifications.duplicate_error')); } } catch (error) { - notify('error', 'An error occurred while updating the Target URL.'); + notify('error', t('robot_duplication.notifications.unknown_error')); console.error('Error updating Target URL:', error); } }; @@ -126,34 +122,40 @@ export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initia modalStyle={modalStyle} > <> - Duplicate Robot + + {t('robot_duplication.title')} + { robot && ( <> - Robot duplication is useful to extract data from pages with the same structure. + + {t('robot_duplication.descriptions.purpose')} + +
+ producthunt.com/topics/api', + url2: 'producthunt.com/topics/database' + }) + }}/>
- Example: If you've created a robot for producthunt.com/topics/api, you can duplicate it to scrape similar pages - like producthunt.com/topics/database without training a robot from scratch. - -
- - ⚠️ Ensure the new page has the same structure as the original page. + {t('robot_duplication.descriptions.warning')} diff --git a/src/components/molecules/RobotEdit.tsx b/src/components/molecules/RobotEdit.tsx index 9441ecef..73397da4 100644 --- a/src/components/molecules/RobotEdit.tsx +++ b/src/components/molecules/RobotEdit.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { GenericModal } from "../atoms/GenericModal"; import { TextField, Typography, Box, Button } from "@mui/material"; import { modalStyle } from "./AddWhereCondModal"; @@ -54,10 +55,10 @@ interface RobotSettingsProps { handleStart: (settings: RobotSettings) => void; handleClose: () => void; initialSettings?: RobotSettings | null; - } export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => { + const { t } = useTranslation(); const [robot, setRobot] = useState(null); const { recordingId, notify } = useGlobalInfoStore(); @@ -72,7 +73,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin const robot = await getStoredRecording(recordingId); setRobot(robot); } else { - notify('error', 'Could not find robot details. Please try again.'); + notify('error', t('robot_edit.notifications.update_failed')); } } @@ -102,6 +103,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin return { ...prev, recording: { ...prev.recording, workflow: updatedWorkflow } }; }); }; + const handleSave = async () => { if (!robot) return; @@ -114,7 +116,7 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin const success = await updateRecording(robot.recording_meta.id, payload); if (success) { - notify('success', 'Robot updated successfully.'); + notify('success', t('robot_edit.notifications.update_success')); handleStart(robot); // Inform parent about the updated robot handleClose(); @@ -122,10 +124,10 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin window.location.reload(); }, 1000); } else { - notify('error', 'Failed to update the robot. Please try again.'); + notify('error', t('robot_edit.notifications.update_failed')); } } catch (error) { - notify('error', 'An error occurred while updating the robot.'); + notify('error', t('robot_edit.notifications.update_error')); console.error('Error updating robot:', error); } }; @@ -137,13 +139,15 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin modalStyle={modalStyle} > <> - Edit Robot + + {t('robot_edit.title')} + { robot && ( <> {robot.recording.workflow?.[0]?.what?.[0]?.args?.[0]?.limit !== undefined && ( { @@ -168,10 +172,15 @@ export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettin - diff --git a/src/components/molecules/RobotSettings.tsx b/src/components/molecules/RobotSettings.tsx index 53d21d7b..d952f43d 100644 --- a/src/components/molecules/RobotSettings.tsx +++ b/src/components/molecules/RobotSettings.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { GenericModal } from "../atoms/GenericModal"; import { TextField, Typography, Box } from "@mui/material"; import { modalStyle } from "./AddWhereCondModal"; @@ -50,10 +51,10 @@ interface RobotSettingsProps { handleStart: (settings: RobotSettings) => void; handleClose: () => void; initialSettings?: RobotSettings | null; - } export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => { + const { t } = useTranslation(); const [robot, setRobot] = useState(null); const [userEmail, setUserEmail] = useState(null); const { recordingId, notify } = useGlobalInfoStore(); @@ -69,7 +70,7 @@ export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSe const robot = await getStoredRecording(recordingId); setRobot(robot); } else { - notify('error', 'Could not find robot details. Please try again.'); + notify('error', t('robot_settings.errors.robot_not_found')); } } @@ -97,13 +98,15 @@ export const RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSe modalStyle={modalStyle} > <> - Robot Settings + + {t('robot_settings.title')} + { robot && ( <> {robot.recording.workflow?.[0]?.what?.[0]?.args?.[0]?.limit !== undefined && ( )} ); -}; +}; \ No newline at end of file diff --git a/src/components/molecules/RunContent.tsx b/src/components/molecules/RunContent.tsx index ff414628..dff44c6c 100644 --- a/src/components/molecules/RunContent.tsx +++ b/src/components/molecules/RunContent.tsx @@ -13,6 +13,7 @@ import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import 'highlight.js/styles/github.css'; +import { useTranslation } from "react-i18next"; interface RunContentProps { row: Data, @@ -23,6 +24,7 @@ interface RunContentProps { } export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => { + const { t } = useTranslation(); const [tab, setTab] = React.useState('log'); const [tableData, setTableData] = useState([]); const [columns, setColumns] = useState([]); @@ -76,8 +78,8 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe setTab(newTab)} aria-label="run-content-tabs"> - - + + @@ -102,32 +104,32 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe color="error" onClick={abortRunHandler} > - Stop + {t('run_content.buttons.stop')} : null} {!row || !row.serializableOutput || !row.binaryOutput || (Object.keys(row.serializableOutput).length === 0 && Object.keys(row.binaryOutput).length === 0) - ? The output is empty. : null} + ? {t('run_content.empty_output')} : null} {row.serializableOutput && Object.keys(row.serializableOutput).length !== 0 &&
- Captured Data + {t('run_content.captured_data.title')} - Download as JSON + {t('run_content.captured_data.download_json')} - Download as CSV + {t('run_content.captured_data.download_csv')} {tableData.length > 0 ? ( @@ -171,7 +173,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
- Captured Screenshot + {t('run_content.captured_screenshot.title')} {Object.keys(row.binaryOutput).map((key) => { try { @@ -181,7 +183,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe width: 'max-content', }}> - Download Screenshot + {t('run_content.captured_screenshot.download')} {key} @@ -189,7 +191,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe } catch (e) { console.log(e) return - {key}: The image failed to render + {key}: {t('run_content.captured_screenshot.render_failed')} } })} diff --git a/src/components/molecules/RunsTable.tsx b/src/components/molecules/RunsTable.tsx index 669cecd6..61e139ec 100644 --- a/src/components/molecules/RunsTable.tsx +++ b/src/components/molecules/RunsTable.tsx @@ -1,4 +1,6 @@ import * as React from 'react'; +import { useEffect, useState } from "react"; +import { useTranslation } from 'react-i18next'; import Paper from '@mui/material/Paper'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; @@ -7,14 +9,24 @@ import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TablePagination from '@mui/material/TablePagination'; import TableRow from '@mui/material/TableRow'; -import { useEffect, useState } from "react"; +import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField } from '@mui/material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import SearchIcon from '@mui/icons-material/Search'; + import { useGlobalInfoStore } from "../../context/globalInfo"; import { getStoredRuns } from "../../api/storage"; import { RunSettings } from "./RunSettings"; import { CollapsibleRow } from "./ColapsibleRow"; -import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField } from '@mui/material'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import SearchIcon from '@mui/icons-material/Search'; + +// Export columns before the component +export const columns: readonly Column[] = [ + { id: 'runStatus', label: 'Status', minWidth: 80 }, + { id: 'name', label: 'Name', minWidth: 80 }, + { id: 'startedAt', label: 'Started At', minWidth: 80 }, + { id: 'finishedAt', label: 'Finished At', minWidth: 80 }, + { id: 'settings', label: 'Settings', minWidth: 80 }, + { id: 'delete', label: 'Delete', minWidth: 80 }, +]; interface Column { id: 'runStatus' | 'name' | 'startedAt' | 'finishedAt' | 'delete' | 'settings'; @@ -24,15 +36,6 @@ interface Column { format?: (value: string) => string; } -export const columns: readonly Column[] = [ - { id: 'runStatus', label: 'Status', minWidth: 80 }, - { id: 'name', label: 'Robot Name', minWidth: 80 }, - { id: 'startedAt', label: 'Started at', minWidth: 80 }, - { id: 'finishedAt', label: 'Finished at', minWidth: 80 }, - { id: 'settings', label: 'Settings', minWidth: 80 }, - { id: 'delete', label: 'Delete', minWidth: 80 }, -]; - export interface Data { id: number; status: string; @@ -58,15 +61,25 @@ interface RunsTableProps { runningRecordingName: string; } -export const RunsTable = ( - { currentInterpretationLog, abortRunHandler, runId, runningRecordingName }: RunsTableProps) => { +export const RunsTable: React.FC = ({ + currentInterpretationLog, + abortRunHandler, + runId, + runningRecordingName +}) => { + const { t } = useTranslation(); + + // Update column labels using translation if needed + const translatedColumns = columns.map(column => ({ + ...column, + label: t(`runstable.${column.id}`, column.label) + })); + const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); const [rows, setRows] = useState([]); - const [searchTerm, setSearchTerm] = useState(''); - const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore(); const handleChangePage = (event: unknown, newPage: number) => { @@ -86,16 +99,13 @@ export const RunsTable = ( const fetchRuns = async () => { const runs = await getStoredRuns(); if (runs) { - const parsedRows: Data[] = []; - runs.map((run: any, index) => { - parsedRows.push({ - id: index, - ...run, - }); - }); + const parsedRows: Data[] = runs.map((run: any, index: number) => ({ + id: index, + ...run, + })); setRows(parsedRows); } else { - notify('error', 'No runs found. Please try again.') + notify('error', t('runstable.notifications.no_runs')); } }; @@ -104,15 +114,14 @@ export const RunsTable = ( fetchRuns(); setRerenderRuns(false); } - }, [rerenderRuns]); + }, [rerenderRuns, rows.length, setRerenderRuns]); const handleDelete = () => { setRows([]); - notify('success', 'Run deleted successfully'); + notify('success', t('runstable.notifications.delete_success')); fetchRuns(); }; - // Filter rows based on search term const filteredRows = rows.filter((row) => row.name.toLowerCase().includes(searchTerm.toLowerCase()) @@ -120,7 +129,6 @@ export const RunsTable = ( // Group filtered rows by robot meta id const groupedRows = filteredRows.reduce((acc, row) => { - if (!acc[row.robotMetaId]) { acc[row.robotMetaId] = []; } @@ -132,11 +140,11 @@ export const RunsTable = ( - All Runs + {t('runstable.runs', 'Runs')} ( }> - {data[data.length - 1].name} - - {columns.map((column) => ( + {translatedColumns.map((column) => ( ); -}; +}; \ No newline at end of file diff --git a/src/components/molecules/SaveRecording.tsx b/src/components/molecules/SaveRecording.tsx index cfebc867..8e1eb462 100644 --- a/src/components/molecules/SaveRecording.tsx +++ b/src/components/molecules/SaveRecording.tsx @@ -9,13 +9,14 @@ import { TextField, Typography } from "@mui/material"; import { WarningText } from "../atoms/texts"; import NotificationImportantIcon from "@mui/icons-material/NotificationImportant"; import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; interface SaveRecordingProps { fileName: string; } export const SaveRecording = ({ fileName }: SaveRecordingProps) => { - + const { t } = useTranslation(); const [openModal, setOpenModal] = useState(false); const [needConfirm, setNeedConfirm] = useState(false); const [recordingName, setRecordingName] = useState(fileName); @@ -46,7 +47,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => { }; const exitRecording = useCallback(async () => { - notify('success', 'Robot saved successfully'); + notify('success', t('save_recording.notifications.save_success')); if (browserId) { await stopRecording(browserId); } @@ -63,7 +64,7 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => { setWaitingForSave(true); console.log(`Saving the recording as ${recordingName} for userId ${user.id}`); } else { - console.error('User not logged in. Cannot save recording.'); + console.error(t('save_recording.notifications.user_not_logged')); } }; @@ -77,34 +78,38 @@ export const SaveRecording = ({ fileName }: SaveRecordingProps) => { return (
setOpenModal(false)} modalStyle={modalStyle}>
- Save Robot + {t('save_recording.title')} {needConfirm ? ( - + - Robot with this name already exists, please confirm the Robot's overwrite. + {t('save_recording.warnings.robot_exists')} ) - : + : } {waitingForSave && - + diff --git a/src/components/molecules/ScheduleSettings.tsx b/src/components/molecules/ScheduleSettings.tsx index 00e842ea..3af0072f 100644 --- a/src/components/molecules/ScheduleSettings.tsx +++ b/src/components/molecules/ScheduleSettings.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { GenericModal } from "../atoms/GenericModal"; import { MenuItem, TextField, Typography, Box } from "@mui/material"; import { Dropdown } from "../atoms/DropdownMui"; @@ -25,6 +26,7 @@ export interface ScheduleSettings { } export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initialSettings }: ScheduleSettingsProps) => { + const { t } = useTranslation(); const [schedule, setSchedule] = useState(null); const [settings, setSettings] = useState({ runEvery: 1, @@ -116,6 +118,25 @@ export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initia } }, [isOpen]); + const getDayOrdinal = (day: string | undefined) => { + if (!day) return ''; + const lastDigit = day.slice(-1); + const lastTwoDigits = day.slice(-2); + + // Special cases for 11, 12, 13 + if (['11', '12', '13'].includes(lastTwoDigits)) { + return t('schedule_settings.labels.on_day.th'); + } + + // Other cases + switch (lastDigit) { + case '1': return t('schedule_settings.labels.on_day.st'); + case '2': return t('schedule_settings.labels.on_day.nd'); + case '3': return t('schedule_settings.labels.on_day.rd'); + default: return t('schedule_settings.labels.on_day.th'); + } + }; + return ( *': { marginBottom: '20px' }, }}> - Schedule Settings + {t('schedule_settings.title')} <> {schedule !== null ? ( <> - Run every: {schedule.runEvery} {schedule.runEveryUnit.toLowerCase()} - {['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? "Start From" : "On"} {schedule.startFrom.charAt(0).toUpperCase() + schedule.startFrom.slice(1).toLowerCase()} + {t('schedule_settings.run_every')}: {schedule.runEvery} {schedule.runEveryUnit.toLowerCase()} + {['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? t('schedule_settings.start_from') : t('schedule_settings.start_from')}: {schedule.startFrom.charAt(0).toUpperCase() + schedule.startFrom.slice(1).toLowerCase()} {schedule.runEveryUnit === 'MONTHS' && ( - On day: {schedule.dayOfMonth}{['1', '21', '31'].includes(schedule.dayOfMonth || '') ? 'st' : ['2', '22'].includes(schedule.dayOfMonth || '') ? 'nd' : ['3', '23'].includes(schedule.dayOfMonth || '') ? 'rd' : 'th'} of the month + {t('schedule_settings.on_day')}: {schedule.dayOfMonth}{getDayOrdinal(schedule.dayOfMonth)} of the month )} - At around: {schedule.atTimeStart}, {schedule.timezone} Timezone + {t('schedule_settings.at_around')}: {schedule.atTimeStart}, {schedule.timezone} {t('schedule_settings.timezone')} ) : ( <> - Run once every + {t('schedule_settings.labels.run_once_every')} - {['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? "Start From" : "On"} + + {['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? t('schedule_settings.labels.start_from_label') : t('schedule_settings.labels.start_from_label')} + - On Day of the Month + {t('schedule_settings.labels.on_day_of_month')} - In Between + {t('schedule_settings.labels.in_between')} ) : ( - At Around + {t('schedule_settings.at_around')} - Timezone + {t('schedule_settings.timezone')} @@ -271,4 +294,4 @@ const modalStyle = { height: 'fit-content', display: 'block', padding: '20px', -}; +}; \ No newline at end of file diff --git a/src/components/organisms/ApiKey.tsx b/src/components/organisms/ApiKey.tsx index e6a00a91..37a72764 100644 --- a/src/components/organisms/ApiKey.tsx +++ b/src/components/organisms/ApiKey.tsx @@ -19,6 +19,7 @@ import styled from 'styled-components'; import axios from 'axios'; import { useGlobalInfoStore } from '../../context/globalInfo'; import { apiUrl } from '../../apiConfig'; +import { useTranslation } from 'react-i18next'; const Container = styled(Box)` display: flex; @@ -29,24 +30,21 @@ const Container = styled(Box)` `; const ApiKeyManager = () => { + const { t } = useTranslation(); const [apiKey, setApiKey] = useState(null); - const [apiKeyName, setApiKeyName] = useState('Maxun API Key'); + const [apiKeyName, setApiKeyName] = useState(t('apikey.default_name')); const [loading, setLoading] = useState(true); const [showKey, setShowKey] = useState(false); const [copySuccess, setCopySuccess] = useState(false); const { notify } = useGlobalInfoStore(); - - - - useEffect(() => { const fetchApiKey = async () => { try { const { data } = await axios.get(`${apiUrl}/auth/api-key`); setApiKey(data.api_key); } catch (error: any) { - notify('error', `Failed to fetch API Key - ${error.message}`); + notify('error', t('apikey.notifications.fetch_error', { error: error.message })); } finally { setLoading(false); } @@ -62,9 +60,9 @@ const ApiKeyManager = () => { const { data } = await axios.post(`${apiUrl}/auth/generate-api-key`); setApiKey(data.api_key); - notify('success', `Generated API Key successfully`); + notify('success', t('apikey.notifications.generate_success')); } catch (error: any) { - notify('error', `Failed to generate API Key - ${error.message}`); + notify('error', t('apikey.notifications.generate_error', { error: error.message })); } finally { setLoading(false); } @@ -75,9 +73,9 @@ const ApiKeyManager = () => { try { await axios.delete(`${apiUrl}/auth/delete-api-key`); setApiKey(null); - notify('success', 'API Key deleted successfully'); + notify('success', t('apikey.notifications.delete_success')); } catch (error: any) { - notify('error', `Failed to delete API Key - ${error.message}`); + notify('error', t('apikey.notifications.delete_error', { error: error.message })); } finally { setLoading(false); } @@ -88,7 +86,7 @@ const ApiKeyManager = () => { navigator.clipboard.writeText(apiKey); setCopySuccess(true); setTimeout(() => setCopySuccess(false), 2000); - notify('info', 'Copied API Key successfully'); + notify('info', t('apikey.notifications.copy_success')); } }; @@ -111,16 +109,16 @@ const ApiKeyManager = () => { return ( - Manage Your API Key + {t('apikey.title')} {apiKey ? (
- API Key Name - API Key - Actions + {t('apikey.table.name')} + {t('apikey.table.key')} + {t('apikey.table.actions')} @@ -128,17 +126,17 @@ const ApiKeyManager = () => { {apiKeyName} {showKey ? `${apiKey?.substring(0, 10)}...` : '***************'} - + - + setShowKey(!showKey)}> - + @@ -150,9 +148,9 @@ const ApiKeyManager = () => { ) : ( <> - You haven't generated an API key yet. + {t('apikey.no_key_message')} )} diff --git a/src/components/organisms/BrowserWindow.tsx b/src/components/organisms/BrowserWindow.tsx index 697b4adb..c7e9fc0f 100644 --- a/src/components/organisms/BrowserWindow.tsx +++ b/src/components/organisms/BrowserWindow.tsx @@ -7,6 +7,7 @@ import { GenericModal } from '../atoms/GenericModal'; import { useActionContext } from '../../context/browserActions'; import { useBrowserSteps, TextStep } from '../../context/browserSteps'; import { useGlobalInfoStore } from '../../context/globalInfo'; +import { useTranslation } from 'react-i18next'; interface ElementInfo { @@ -52,6 +53,7 @@ const getAttributeOptions = (tagName: string, elementInfo: ElementInfo | null): }; export const BrowserWindow = () => { + const { t } = useTranslation(); const [canvasRef, setCanvasReference] = useState | undefined>(undefined); const [screenShot, setScreenShot] = useState(""); const [highlighterData, setHighlighterData] = useState<{ rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] } | null>(null); @@ -200,7 +202,7 @@ export const BrowserWindow = () => { // Only allow selection in pagination mode if type is not empty, 'scrollDown', or 'scrollUp' if (paginationType !== '' && paginationType !== 'scrollDown' && paginationType !== 'scrollUp' && paginationType !== 'none') { setPaginationSelector(highlighterData.selector); - notify(`info`, `Pagination element selected successfully.`); + notify(`info`, t('browser_window.attribute_modal.notifications.pagination_select_success')); addListStep(listSelector!, fields, currentListId || 0, { type: paginationType, selector: highlighterData.selector }); } return; @@ -208,7 +210,7 @@ export const BrowserWindow = () => { if (getList === true && !listSelector) { setListSelector(highlighterData.selector); - notify(`info`, `List selected succesfully. Select the text data for extraction.`) + notify(`info`, t('browser_window.attribute_modal.notifications.list_select_success')); setCurrentListId(Date.now()); setFields({}); } else if (getList === true && listSelector && currentListId) { diff --git a/src/components/organisms/MainMenu.tsx b/src/components/organisms/MainMenu.tsx index dadb6731..4143ae9f 100644 --- a/src/components/organisms/MainMenu.tsx +++ b/src/components/organisms/MainMenu.tsx @@ -5,6 +5,9 @@ import Box from '@mui/material/Box'; import { Paper, Button } from "@mui/material"; import { AutoAwesome, FormatListBulleted, VpnKey, Usb, CloudQueue, Code } from "@mui/icons-material"; import { apiUrl } from "../../apiConfig"; +import { useTranslation } from 'react-i18next'; +import i18n from '../../i18n'; + interface MainMenuProps { value: string; @@ -12,6 +15,7 @@ interface MainMenuProps { } export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenuProps) => { + const {t} = useTranslation(); const handleChange = (event: React.SyntheticEvent, newValue: string) => { handleChangeContent(newValue); @@ -47,7 +51,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu fontSize: 'medium', }} value="recordings" - label="Robots" + label={t('mainmenu.recordings')} icon={} iconPosition="start" /> @@ -58,7 +62,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu fontSize: 'medium', }} value="runs" - label="Runs" + label={t('mainmenu.runs')} icon={} iconPosition="start" /> @@ -69,7 +73,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu fontSize: 'medium', }} value="proxy" - label="Proxy" + label={t('mainmenu.proxy')} icon={} iconPosition="start" /> @@ -80,7 +84,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu fontSize: 'medium', }} value="apikey" - label="API Key" + label={t('mainmenu.apikey')} icon={} iconPosition="start" /> @@ -88,10 +92,10 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
diff --git a/src/components/organisms/ProxyForm.tsx b/src/components/organisms/ProxyForm.tsx index a581144b..46874349 100644 --- a/src/components/organisms/ProxyForm.tsx +++ b/src/components/organisms/ProxyForm.tsx @@ -3,6 +3,7 @@ import { styled } from '@mui/system'; import { Alert, AlertTitle, TextField, Button, Switch, FormControlLabel, Box, Typography, Tabs, Tab, Table, TableContainer, TableHead, TableRow, TableBody, TableCell, Paper } from '@mui/material'; import { sendProxyConfig, getProxyConfig, testProxyConfig, deleteProxyConfig } from '../../api/proxy'; import { useGlobalInfoStore } from '../../context/globalInfo'; +import { useTranslation } from 'react-i18next'; const FormContainer = styled(Box)({ display: 'flex', @@ -16,6 +17,7 @@ const FormControl = styled(Box)({ }); const ProxyForm: React.FC = () => { + const { t } = useTranslation(); const [proxyConfigForm, setProxyConfigForm] = useState({ server_url: '', username: '', @@ -79,13 +81,13 @@ const ProxyForm: React.FC = () => { try { const response = await sendProxyConfig(proxyConfigForm); if (response) { - notify('success', 'Proxy configuration submitted successfully'); + notify('success', t('proxy.notifications.config_success')); } else { - notify('error', `Failed to submit proxy configuration. Try again. ${response}`); - console.log(`Failed to submit proxy configuration. Try again. ${response}`) + notify('error', t('proxy.notifications.config_error')); + console.log(`${t('proxy.notifications.config_error')} ${response}`) } } catch (error: any) { - notify('error', `${error} : Failed to submit proxy configuration`); + notify('error', `${error} : ${t('proxy.notifications.config_error')}`); } }; @@ -96,9 +98,9 @@ const ProxyForm: React.FC = () => { const testProxy = async () => { await testProxyConfig().then((response) => { if (response.success) { - notify('success', 'Proxy configuration is working'); + notify('success', t('proxy.notifications.test_success')); } else { - notify('error', 'Failed to test proxy configuration. Try again.'); + notify('error', t('proxy.notifications.test_error')); } }); }; @@ -109,7 +111,7 @@ const ProxyForm: React.FC = () => { if (response.proxy_url) { setIsProxyConfigured(true); setProxy(response); - notify('success', 'Proxy configuration fetched successfully'); + notify('success', t('proxy.notifications.fetch_success')); } } catch (error: any) { notify('error', error); @@ -119,11 +121,11 @@ const ProxyForm: React.FC = () => { const removeProxy = async () => { await deleteProxyConfig().then((response) => { if (response) { - notify('success', 'Proxy configuration removed successfully'); + notify('success', t('proxy.notifications.remove_success')); setIsProxyConfigured(false); setProxy({ proxy_url: '', auth: false }); } else { - notify('error', 'Failed to remove proxy configuration. Try again.'); + notify('error', t('proxy.notifications.remove_error')); } }); } @@ -136,11 +138,11 @@ const ProxyForm: React.FC = () => { <> - Proxy Configuration + {t('proxy.title')} - - + + {tabIndex === 0 && ( isProxyConfigured ? ( @@ -149,8 +151,8 @@ const ProxyForm: React.FC = () => {
- Proxy URL - Requires Authentication + {t('proxy.table.proxy_url')} + {t('proxy.table.requires_auth')} @@ -162,39 +164,37 @@ const ProxyForm: React.FC = () => {
) : ( } - label="Requires Authentication?" + label={t('proxy.requires_auth')} /> {requiresAuth && ( <> { { fullWidth disabled={!proxyConfigForm.server_url || (requiresAuth && (!proxyConfigForm.username || !proxyConfigForm.password))} > - Add Proxy + {t('proxy.add_proxy')} ))} @@ -234,33 +234,33 @@ const ProxyForm: React.FC = () => { <> - Coming Soon - In Open Source (Basic Rotation) & Cloud (Advanced Rotation). If you don't want to manage the infrastructure, join our cloud waitlist to get early access. + {t('proxy.coming_soon')} )} - If your proxy requires a username and password, always provide them separately from the proxy URL. + {t('proxy.alert.title')}
- The right way + {t('proxy.alert.right_way')}
- Proxy URL: http://proxy.com:1337 + {t('proxy.alert.proxy_url')} http://proxy.com:1337
- Username: myusername + {t('proxy.alert.username')} myusername
- Password: mypassword + {t('proxy.alert.password')} mypassword

- The wrong way + {t('proxy.alert.wrong_way')}
- Proxy URL: http://myusername:mypassword@proxy.com:1337 + {t('proxy.alert.proxy_url')} http://myusername:mypassword@proxy.com:1337
); }; -export default ProxyForm; +export default ProxyForm; \ No newline at end of file diff --git a/src/components/organisms/RightSidePanel.tsx b/src/components/organisms/RightSidePanel.tsx index 4aaf7b21..224e0954 100644 --- a/src/components/organisms/RightSidePanel.tsx +++ b/src/components/organisms/RightSidePanel.tsx @@ -22,6 +22,7 @@ import { emptyWorkflow } from "../../shared/constants"; import { getActiveWorkflow } from "../../api/workflow"; import DeleteIcon from '@mui/icons-material/Delete'; import ActionDescriptionBox from '../molecules/ActionDescriptionBox'; +import { useTranslation } from 'react-i18next'; const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => { getActiveWorkflow(id).then( @@ -60,6 +61,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture const { getText, startGetText, stopGetText, getScreenshot, startGetScreenshot, stopGetScreenshot, getList, startGetList, stopGetList, startPaginationMode, stopPaginationMode, paginationType, updatePaginationType, limitType, customLimit, updateLimitType, updateCustomLimit, stopLimitMode, startLimitMode, captureStage, setCaptureStage } = useActionContext(); const { browserSteps, updateBrowserTextStepLabel, deleteBrowserStep, addScreenshotStep, updateListTextFieldLabel, removeListTextField } = useBrowserSteps(); const { id, socket } = useSocketStore(); + const { t } = useTranslation(); const workflowHandler = useCallback((data: WorkflowFile) => { setWorkflow(data); @@ -139,7 +141,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture setTextLabels(prevLabels => ({ ...prevLabels, [id]: label })); } if (!label.trim()) { - setErrors(prevErrors => ({ ...prevErrors, [id]: 'Label cannot be empty' })); + setErrors(prevErrors => ({ ...prevErrors, [id]: t('right_panel.errors.label_required') })); } else { setErrors(prevErrors => ({ ...prevErrors, [id]: '' })); } @@ -151,7 +153,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture updateBrowserTextStepLabel(id, label); setConfirmedTextSteps(prev => ({ ...prev, [id]: true })); } else { - setErrors(prevErrors => ({ ...prevErrors, [id]: 'Label cannot be empty' })); + setErrors(prevErrors => ({ ...prevErrors, [id]: t('right_panel.errors.label_required') })); } }; @@ -213,7 +215,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture const stopCaptureAndEmitGetTextSettings = useCallback(() => { const hasUnconfirmedTextSteps = browserSteps.some(step => step.type === 'text' && !confirmedTextSteps[step.id]); if (hasUnconfirmedTextSteps) { - notify('error', 'Please confirm all text fields'); + notify('error', t('right_panel.errors.confirm_text_fields')); return; } stopGetText(); @@ -278,7 +280,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture if (settings) { socket?.emit('action', { action: 'scrapeList', settings }); } else { - notify('error', 'Unable to create list settings. Make sure you have defined a field for the list.'); + notify('error', t('right_panel.errors.unable_create_settings')); } handleStopGetList(); onFinishCapture(); @@ -296,13 +298,13 @@ export const RightSidePanel: React.FC = ({ onFinishCapture case 'pagination': if (!paginationType) { - notify('error', 'Please select a pagination type.'); + notify('error', t('right_panel.errors.select_pagination')); return; } const settings = getListSettingsObject(); const paginationSelector = settings.pagination?.selector; if (['clickNext', 'clickLoadMore'].includes(paginationType) && !paginationSelector) { - notify('error', 'Please select the pagination element first.'); + notify('error', t('right_panel.errors.select_pagination_element')); return; } stopPaginationMode(); @@ -314,12 +316,12 @@ export const RightSidePanel: React.FC = ({ onFinishCapture case 'limit': if (!limitType || (limitType === 'custom' && !customLimit)) { - notify('error', 'Please select a limit or enter a custom limit.'); + notify('error', t('right_panel.errors.select_limit')); return; } const limit = limitType === 'custom' ? parseInt(customLimit) : parseInt(limitType); if (isNaN(limit) || limit <= 0) { - notify('error', 'Please enter a valid limit.'); + notify('error', t('right_panel.errors.invalid_limit')); return; } stopLimitMode(); @@ -348,7 +350,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture setTextLabels({}); setErrors({}); setConfirmedTextSteps({}); - notify('error', 'Capture Text Discarded'); + notify('error', t('right_panel.errors.capture_text_discarded')); }, [browserSteps, stopGetText, deleteBrowserStep]); const discardGetList = useCallback(() => { @@ -363,7 +365,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture setShowLimitOptions(false); setCaptureStage('initial'); setConfirmedListTextFields({}); - notify('error', 'Capture List Discarded'); + notify('error', t('right_panel.errors.capture_list_discarded')); }, [browserSteps, stopGetList, deleteBrowserStep, resetListState]); @@ -402,7 +404,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture */} - {!getText && !getScreenshot && !getList && showCaptureList && } + {!getText && !getScreenshot && !getList && showCaptureList && } {getList && ( <> @@ -411,28 +413,29 @@ export const RightSidePanel: React.FC = ({ onFinishCapture onClick={handleConfirmListCapture} disabled={captureStage === 'initial' ? isConfirmCaptureDisabled : hasUnconfirmedListTextFields} > - {captureStage === 'initial' ? 'Confirm Capture' : - captureStage === 'pagination' ? 'Confirm Pagination' : - captureStage === 'limit' ? 'Confirm Limit' : 'Finish Capture'} + {captureStage === 'initial' ? t('right_panel.buttons.confirm_capture') : + captureStage === 'pagination' ? t('right_panel.buttons.confirm_pagination') : + captureStage === 'limit' ? t('right_panel.buttons.confirm_limit') : + t('right_panel.buttons.finish_capture')} - + )} {showPaginationOptions && ( - How can we find the next list item on the page? - - - - - + {t('right_panel.pagination.title')} + + + + + )} {showLimitOptions && ( -

What is the maximum number of rows you want to extract?

+

{t('right_panel.limit.title')}

= ({ onFinishCapture } label="10" /> } label="100" />
- } label="Custom" /> + } label={t('right_panel.limit.custom')} /> {limitType === 'custom' && ( updateCustomLimit(e.target.value)} - placeholder="Enter number" + placeholder={t('right_panel.limit.enter_number')} sx={{ marginLeft: '10px', '& input': { @@ -467,21 +470,21 @@ export const RightSidePanel: React.FC = ({ onFinishCapture )} - {!getText && !getScreenshot && !getList && showCaptureText && } + {!getText && !getScreenshot && !getList && showCaptureText && } {getText && <> - - + + } - {!getText && !getScreenshot && !getList && showCaptureScreenshot && } + {!getText && !getScreenshot && !getList && showCaptureScreenshot && } {getScreenshot && ( - - - + + + )} @@ -492,7 +495,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture step.type === 'text' && ( <> handleTextLabelChange(step.id, e.target.value)} fullWidth @@ -510,7 +513,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture }} /> = ({ onFinishCapture /> {!confirmedTextSteps[step.id] && ( - - + + )} @@ -535,17 +538,19 @@ export const RightSidePanel: React.FC = ({ onFinishCapture - {`Take ${step.fullPage ? 'Fullpage' : 'Visible Part'} Screenshot`} + {step.fullPage ? + t('right_panel.screenshot.display_fullpage') : + t('right_panel.screenshot.display_visible')} )} {step.type === 'list' && ( <> - List Selected Successfully + {t('right_panel.messages.list_selected')} {Object.entries(step.fields).map(([key, field]) => ( handleTextLabelChange(field.id, e.target.value, step.id, key)} fullWidth @@ -560,7 +565,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture }} /> = ({ onFinishCapture onClick={() => handleListTextFieldConfirm(step.id, key)} disabled={!field.label?.trim()} > - Confirm + {t('right_panel.buttons.confirm')} )} diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 00000000..c5e84364 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,22 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import Backend from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: 'en', + debug: import.meta.env.DEV, + supportedLngs: ['en', 'es', 'ja', 'zh','de'], + interpolation: { + escapeValue: false, // React already escapes + }, + backend: { + loadPath: '/locales/{{lng}}.json', + }, + }); + +export default i18n; \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 8c14f60a..96f914ff 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'; import './index.css'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; +import i18n from "./i18n" const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 87f90b53..3c8e08c4 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,134 +1,142 @@ -import axios from "axios"; -import { useState, useContext, useEffect, FormEvent } from "react"; -import { useNavigate, Link } from "react-router-dom"; -import { AuthContext } from "../context/auth"; -import { Box, Typography, TextField, Button, CircularProgress, Grid } from "@mui/material"; -import { useGlobalInfoStore } from "../context/globalInfo"; +import axios from "axios"; +import { useState, useContext, useEffect, FormEvent } from "react"; +import { useNavigate, Link } from "react-router-dom"; +import { AuthContext } from "../context/auth"; +import { Box, Typography, TextField, Button, CircularProgress, Grid } from "@mui/material"; +import { useGlobalInfoStore } from "../context/globalInfo"; import { apiUrl } from "../apiConfig"; +import { useTranslation } from 'react-i18next'; +import i18n from '../i18n'; const Login = () => { - const [form, setForm] = useState({ - email: "", - password: "", - }); - const [loading, setLoading] = useState(false); - const { notify } = useGlobalInfoStore(); - const { email, password } = form; + const { t } = useTranslation(); + console.log(i18n) + console.log(t) + const [form, setForm] = useState({ + email: "", + password: "", + }); + const [loading, setLoading] = useState(false); + const { notify } = useGlobalInfoStore(); + const { email, password } = form; - const { state, dispatch } = useContext(AuthContext); - const { user } = state; + const { state, dispatch } = useContext(AuthContext); + const { user } = state; - const navigate = useNavigate(); + const navigate = useNavigate(); - useEffect(() => { - if (user) { - navigate("/"); - } - }, [user, navigate]); + useEffect(() => { + if (user) { + navigate("/"); + } + }, [user, navigate]); - const handleChange = (e: any) => { - const { name, value } = e.target; - setForm({ ...form, [name]: value }); - }; + const handleChange = (e: any) => { + const { name, value } = e.target; + setForm({ ...form, [name]: value }); + }; - const submitForm = async (e: any) => { - e.preventDefault(); - setLoading(true); - try { - const { data } = await axios.post(`${apiUrl}/auth/login`, { - email, - password, - }); - dispatch({ type: "LOGIN", payload: data }); - notify("success", "Welcome to Maxun!"); - window.localStorage.setItem("user", JSON.stringify(data)); - navigate("/"); - } catch (err) { - notify("error", "Login Failed. Please try again."); - setLoading(false); - } - }; + const submitForm = async (e: any) => { + e.preventDefault(); + setLoading(true); + try { + const { data } = await axios.post(`${apiUrl}/auth/login`, { + email, + password, + }); + dispatch({ type: "LOGIN", payload: data }); + notify("success", t('login.welcome_notification')); // Translated notification + window.localStorage.setItem("user", JSON.stringify(data)); + navigate("/"); + } catch (err) { + notify("error", t('login.error_notification')); // Translated error + setLoading(false); + } + }; - return ( - - - - logo - - Welcome Back! - - - - - - Don’t have an account?{" "} - - Register - - - - - - ); + // Language switcher function + + + return ( + + {/* Language Switcher Buttons */} + + + logo + + {t('login.title')} + + + + + + {t('login.register_prompt')}{" "} + + {t('login.register_link')} + + + + + ); }; -export default Login; +export default Login; \ No newline at end of file diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index 8af3d3c5..4a82170e 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { MainMenu } from "../components/organisms/MainMenu"; import { Stack } from "@mui/material"; import { Recordings } from "../components/organisms/Recordings"; @@ -30,7 +31,7 @@ export interface ScheduleRunResponse { } export const MainPage = ({ handleEditRecording }: MainPageProps) => { - + const { t } = useTranslation(); const [content, setContent] = React.useState('recordings'); const [sockets, setSockets] = React.useState([]); const [runningRecordingId, setRunningRecordingId] = React.useState(''); @@ -49,10 +50,10 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => { aborted = true; notifyAboutAbort(runId).then(async (response) => { if (response) { - notify('success', `Interpretation of robot ${runningRecordingName} aborted successfully`); + notify('success', t('main_page.notifications.abort_success', { name: runningRecordingName })); await stopRecording(ids.browserId); } else { - notify('error', `Failed to abort the interpretation of ${runningRecordingName} robot`); + notify('error', t('main_page.notifications.abort_failed', { name: runningRecordingName })); } }) } @@ -67,9 +68,9 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => { interpretStoredRecording(runId).then(async (interpretation: boolean) => { if (!aborted) { if (interpretation) { - notify('success', `Interpretation of robot ${runningRecordingName} succeeded`); + notify('success', t('main_page.notifications.interpretation_success', { name: runningRecordingName })); } else { - notify('success', `Failed to interpret ${runningRecordingName} robot`); + notify('success', t('main_page.notifications.interpretation_failed', { name: runningRecordingName })); // destroy the created browser await stopRecording(browserId); } @@ -98,9 +99,9 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => { socket.on('debugMessage', debugMessageHandler); setContent('runs'); if (browserId) { - notify('info', `Running robot: ${runningRecordingName}`); + notify('info', t('main_page.notifications.run_started', { name: runningRecordingName })); } else { - notify('error', `Failed to run robot: ${runningRecordingName}`); + notify('error', t('main_page.notifications.run_start_failed', { name: runningRecordingName })); } }) return (socket: Socket, browserId: string, runId: string) => { @@ -113,9 +114,9 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => { scheduleStoredRecording(runningRecordingId, settings) .then(({ message, runId }: ScheduleRunResponse) => { if (message === 'success') { - notify('success', `Robot ${runningRecordingName} scheduled successfully`); + notify('success', t('main_page.notifications.schedule_success', { name: runningRecordingName })); } else { - notify('error', `Failed to schedule robot ${runningRecordingName}`); + notify('error', t('main_page.notifications.schedule_failed', { name: runningRecordingName })); } }); } @@ -151,4 +152,4 @@ export const MainPage = ({ handleEditRecording }: MainPageProps) => { {DisplayContent()} ); -}; +}; \ No newline at end of file diff --git a/src/pages/RecordingPage.tsx b/src/pages/RecordingPage.tsx index 18fad49f..82fa7e32 100644 --- a/src/pages/RecordingPage.tsx +++ b/src/pages/RecordingPage.tsx @@ -15,6 +15,7 @@ import { editRecordingFromStorage } from "../api/storage"; import { WhereWhatPair } from "maxun-core"; import styled from "styled-components"; import BrowserRecordingSave from '../components/molecules/BrowserRecordingSave'; +import { useTranslation } from 'react-i18next'; interface RecordingPageProps { recordingName?: string; @@ -26,7 +27,7 @@ export interface PairForEdit { } export const RecordingPage = ({ recordingName }: RecordingPageProps) => { - + const { t } = useTranslation(); const [isLoaded, setIsLoaded] = React.useState(false); const [hasScrollbar, setHasScrollbar] = React.useState(false); const [pairForEdit, setPairForEdit] = useState({ @@ -145,7 +146,7 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => { ) : ( - + )}
diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index c64de4ae..b1d2428f 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -5,8 +5,13 @@ import { AuthContext } from "../context/auth"; import { Box, Typography, TextField, Button, CircularProgress } from "@mui/material"; import { useGlobalInfoStore } from "../context/globalInfo"; import { apiUrl } from "../apiConfig"; +import { useTranslation } from 'react-i18next'; +import i18n from '../i18n'; + + const Register = () => { + const {t} = useTranslation(); const [form, setForm] = useState({ email: "", password: "", @@ -40,11 +45,13 @@ const Register = () => { password, }); dispatch({ type: "LOGIN", payload: data }); - notify("success", "Registration Successful!"); + notify("success", t('register.welcome_notification')); window.localStorage.setItem("user", JSON.stringify(data)); navigate("/"); } catch (error:any) { - notify("error", `Registration Failed. Please try again. ${error.response.data}`); + + notify("error", error.response.data || t('register.error_notification')); + setLoading(false); } }; @@ -78,11 +85,11 @@ const Register = () => { > logo - Create an Account + {t('register.title')} { /> { Loading ) : ( - "Register" + t('register.button') )} - Already have an account?{" "} + {t('register.register_prompt')}{" "} - Login + + {t('register.login_link')}