Maxun
@@ -15,11 +15,11 @@ Maxun lets you train a robot in 2 minutes and scrape the web on auto-pilot. Web
- Website |
+ Website |
Discord |
- Twitter |
+ Twitter |
Join Maxun Cloud |
- Watch Tutorials
+ Watch Tutorials
@@ -30,7 +30,15 @@ Maxun lets you train a robot in 2 minutes and scrape the web on auto-pilot. Web
# Installation
+1. Create a root folder for your project (e.g. 'maxun')
+2. Create a file named `.env` in the root folder of the project
+3. Example env file can be viewed [here](https://github.com/getmaxun/maxun/blob/master/ENVEXAMPLE). Copy all content of example env to your `.env` file.
+4. Choose your installation method below
+
### Docker Compose
+1. Copy paste the [docker-compose.yml file](https://github.com/getmaxun/maxun/blob/master/docker-compose.yml) into your root folder
+2. Ensure you have setup the `.env` file in that same folder
+3. Run the command below from a terminal
```
docker-compose up -d
```
diff --git a/docker-compose.yml b/docker-compose.yml
index 3c6e3a0f..874e48d6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -43,7 +43,7 @@ services:
#build:
#context: .
#dockerfile: server/Dockerfile
- image: getmaxun/maxun-backend:v0.0.7
+ image: getmaxun/maxun-backend:v0.0.9
ports:
- "${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}"
env_file: .env
@@ -70,7 +70,7 @@ services:
#build:
#context: .
#dockerfile: Dockerfile
- image: getmaxun/maxun-frontend:v0.0.3
+ image: getmaxun/maxun-frontend:v0.0.5
ports:
- "${FRONTEND_PORT:-5173}:${FRONTEND_PORT:-5173}"
env_file: .env
diff --git a/maxun-core/package.json b/maxun-core/package.json
index 36d06aa9..7c92d08e 100644
--- a/maxun-core/package.json
+++ b/maxun-core/package.json
@@ -1,6 +1,6 @@
{
"name": "maxun-core",
- "version": "0.0.6",
+ "version": "0.0.7",
"description": "Core package for Maxun, responsible for data extraction",
"main": "build/index.js",
"typings": "build/index.d.ts",
diff --git a/maxun-core/src/browserSide/scraper.js b/maxun-core/src/browserSide/scraper.js
index bb169104..846f68e9 100644
--- a/maxun-core/src/browserSide/scraper.js
+++ b/maxun-core/src/browserSide/scraper.js
@@ -712,7 +712,6 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
return scrapedData;
};
-
/**
* Gets all children of the elements matching the listSelector,
* returning their CSS selectors and innerText.
diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts
index e11ea517..58d0df42 100644
--- a/maxun-core/src/interpret.ts
+++ b/maxun-core/src/interpret.ts
@@ -111,13 +111,21 @@ export default class Interpreter extends EventEmitter {
private async applyAdBlocker(page: Page): Promise {
if (this.blocker) {
- await this.blocker.enableBlockingInPage(page);
+ try {
+ await this.blocker.enableBlockingInPage(page);
+ } catch (err) {
+ this.log(`Ad-blocker operation failed:`, Level.ERROR);
+ }
}
}
private async disableAdBlocker(page: Page): Promise {
if (this.blocker) {
- await this.blocker.disableBlockingInPage(page);
+ try {
+ await this.blocker.disableBlockingInPage(page);
+ } catch (err) {
+ this.log(`Ad-blocker operation failed:`, Level.ERROR);
+ }
}
}
@@ -678,7 +686,11 @@ export default class Interpreter extends EventEmitter {
workflowCopy = this.removeIframeSelectors(workflowCopy);
// apply ad-blocker to the current page
- await this.applyAdBlocker(p);
+ try {
+ await this.applyAdBlocker(p);
+ } catch (error) {
+ this.log(`Failed to apply ad-blocker: ${error.message}`, Level.ERROR);
+ }
const usedActions: string[] = [];
let selectors: string[] = [];
let lastAction = null;
diff --git a/maxun-core/src/utils/concurrency.ts b/maxun-core/src/utils/concurrency.ts
index e1ebb87b..56c15fd9 100644
--- a/maxun-core/src/utils/concurrency.ts
+++ b/maxun-core/src/utils/concurrency.ts
@@ -3,36 +3,36 @@
*/
export default class Concurrency {
/**
- * Maximum number of workers running in parallel. If set to `null`, there is no limit.
- */
+ * Maximum number of workers running in parallel. If set to `null`, there is no limit.
+ */
maxConcurrency: number = 1;
/**
- * Number of currently active workers.
- */
+ * Number of currently active workers.
+ */
activeWorkers: number = 0;
/**
- * Queue of jobs waiting to be completed.
- */
+ * Queue of jobs waiting to be completed.
+ */
private jobQueue: Function[] = [];
/**
- * "Resolve" callbacks of the waitForCompletion() promises.
- */
+ * "Resolve" callbacks of the waitForCompletion() promises.
+ */
private waiting: Function[] = [];
/**
- * Constructs a new instance of concurrency manager.
- * @param {number} maxConcurrency Maximum number of workers running in parallel.
- */
+ * Constructs a new instance of concurrency manager.
+ * @param {number} maxConcurrency Maximum number of workers running in parallel.
+ */
constructor(maxConcurrency: number) {
this.maxConcurrency = maxConcurrency;
}
/**
- * Takes a waiting job out of the queue and runs it.
- */
+ * Takes a waiting job out of the queue and runs it.
+ */
private runNextJob(): void {
const job = this.jobQueue.pop();
@@ -53,12 +53,12 @@ export default class Concurrency {
}
/**
- * Pass a job (a time-demanding async function) to the concurrency manager. \
- * The time of the job's execution depends on the concurrency manager itself
- * (given a generous enough `maxConcurrency` value, it might be immediate,
- * but this is not guaranteed).
- * @param worker Async function to be executed (job to be processed).
- */
+ * Pass a job (a time-demanding async function) to the concurrency manager. \
+ * The time of the job's execution depends on the concurrency manager itself
+ * (given a generous enough `maxConcurrency` value, it might be immediate,
+ * but this is not guaranteed).
+ * @param worker Async function to be executed (job to be processed).
+ */
addJob(job: () => Promise): void {
// console.debug("Adding a worker!");
this.jobQueue.push(job);
@@ -72,11 +72,11 @@ export default class Concurrency {
}
/**
- * Waits until there is no running nor waiting job. \
- * If the concurrency manager is idle at the time of calling this function,
- * it waits until at least one job is completed (can be "presubscribed").
- * @returns Promise, resolved after there is no running/waiting worker.
- */
+ * Waits until there is no running nor waiting job. \
+ * If the concurrency manager is idle at the time of calling this function,
+ * it waits until at least one job is completed (can be "presubscribed").
+ * @returns Promise, resolved after there is no running/waiting worker.
+ */
waitForCompletion(): Promise {
return new Promise((res) => {
this.waiting.push(res);
diff --git a/package.json b/package.json
index 9fab1c55..e89f13de 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "maxun",
- "version": "0.0.4",
+ "version": "0.0.5",
"author": "Maxun",
"license": "AGPL-3.0-or-later",
"dependencies": {
@@ -36,6 +36,9 @@
"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",
@@ -43,7 +46,7 @@
"jwt-decode": "^4.0.0",
"loglevel": "^1.8.0",
"loglevel-plugin-remote": "^0.6.8",
- "maxun-core": "^0.0.6",
+ "maxun-core": "^0.0.7",
"minio": "^8.0.1",
"moment-timezone": "^0.5.45",
"node-cron": "^3.0.3",
@@ -57,6 +60,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..b9b4185b
--- /dev/null
+++ b/public/locales/de.json
@@ -0,0 +1,492 @@
+{
+ "login": {
+ "title": "Willkommen zurück!",
+ "email": "Geben Sie Ihre geschäftliche E-Mail-Adresse ein",
+ "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": "Geben Sie Ihre geschäftliche E-Mail-Adresse ein",
+ "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": "Bestätigen",
+ "confirm_limit": "Bestätigen",
+ "finish_capture": "Erfassung abschließen",
+ "back": "Zurück",
+ "finish": "Fertig",
+ "cancel": "Abbrechen",
+ "delete": "Löschen"
+ },
+ "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": "--------------------------------------------------"
+ },
+ "notifications": {
+ "reset_success": "Vorschau erfolgreich zurückgesetzt"
+ }
+ },
+ "interpretation_buttons": {
+ "buttons": {
+ "preview": "Vorschau der Ausgabedaten anzeigen",
+ "reset": "Zurücksetzen",
+ "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..bd8acce3
--- /dev/null
+++ b/public/locales/en.json
@@ -0,0 +1,502 @@
+{
+ "login": {
+ "title": "Welcome Back!",
+ "email": "Enter Work 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": "Enter Work 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",
+ "confirm_limit": "Confirm",
+ "finish_capture": "Finish Capture",
+ "back": "Back",
+ "finish": "Finish",
+ "cancel": "Cancel",
+ "delete": "Delete"
+ },
+ "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": "--------------------------------------------------"
+ },
+ "notifications": {
+ "reset_success": "Output Preview reset successfully"
+ }
+ },
+ "interpretation_buttons": {
+ "buttons": {
+ "preview": "Get Preview of Output Data",
+ "reset": "Reset",
+ "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": "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": "Select Attribute",
+ "notifications": {
+ "list_select_success": "List has been successfully selected. Please select the text data to extract.",
+ "pagination_select_success": "Pagination element has been successfully selected."
+ }
+ },
+ "attribute_options": {
+ "anchor": {
+ "text": "Text: {{text}}",
+ "url": "URL: {{url}}"
+ },
+ "image": {
+ "alt_text": "Alt Text: {{altText}}",
+ "image_url": "Image URL: {{imageUrl}}"
+ },
+ "default": {
+ "text": "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..94210880
--- /dev/null
+++ b/public/locales/es.json
@@ -0,0 +1,493 @@
+{
+ "login": {
+ "title": "¡Bienvenido de nuevo!",
+ "email": "Introducir correo electrónico de trabajo",
+ "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": "Introducir correo electrónico de trabajo",
+ "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",
+ "confirm_limit": "Confirmar",
+ "finish_capture": "Finalizar Captura",
+ "back": "Atrás",
+ "finish": "Finalizar",
+ "cancel": "Cancelar",
+ "delete": "Eliminar"
+ },
+ "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",
+ "reset": "Restablecer",
+ "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 "
+ },
+ "notifications": {
+ "reset_success": "Vista previa restablecida correctamente"
+ }
+ },
+ "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..0bcba967
--- /dev/null
+++ b/public/locales/ja.json
@@ -0,0 +1,493 @@
+{
+ "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": "取得を完了",
+ "back": "戻る",
+ "finish": "完了",
+ "cancel": "キャンセル",
+ "delete": "削除"
+ },
+ "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": "--------------------------------------------------"
+ },
+ "notifications": {
+ "reset_success": "出力プレビューが正常にリセットされました"
+ }
+ },
+ "interpretation_buttons": {
+ "buttons": {
+ "preview": "出力データのプレビューを取得",
+ "reset": "リセット",
+ "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..a19fe439
--- /dev/null
+++ b/public/locales/zh.json
@@ -0,0 +1,493 @@
+{
+ "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": "完成捕获",
+ "back": "返回",
+ "finish": "完成",
+ "cancel": "取消",
+ "delete": "删除"
+ },
+ "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": "--------------------------------------------------"
+ },
+ "notifications": {
+ "reset_success": "输出预览已成功重置"
+ }
+ },
+ "interpretation_buttons": {
+ "buttons": {
+ "preview": "获取输出数据预览",
+ "reset": "重置",
+ "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/server/Dockerfile b/server/Dockerfile
index e738f252..7e197fd3 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -7,13 +7,14 @@ WORKDIR /app
COPY package*.json ./
COPY maxun-core ./maxun-core
COPY src ./src
+COPY public ./public
COPY server ./server
COPY tsconfig.json ./
COPY server/tsconfig.json ./server/
# COPY server/start.sh ./
# Install dependencies
-RUN npm install
+RUN npm install --legacy-peer-deps
# Install Playwright browsers and dependencies
RUN npx playwright install --with-deps chromium
diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts
index 31aceada..2c45d146 100644
--- a/server/src/browser-management/classes/RemoteBrowser.ts
+++ b/server/src/browser-management/classes/RemoteBrowser.ts
@@ -245,10 +245,17 @@ export class RemoteBrowser {
await this.setupPageEventListeners(this.currentPage);
- const blocker = await PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']);
- await blocker.enableBlockingInPage(this.currentPage);
- this.client = await this.currentPage.context().newCDPSession(this.currentPage);
- await blocker.disableBlockingInPage(this.currentPage);
+ try {
+ const blocker = await PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']);
+ await blocker.enableBlockingInPage(this.currentPage);
+ this.client = await this.currentPage.context().newCDPSession(this.currentPage);
+ await blocker.disableBlockingInPage(this.currentPage);
+ console.log('Adblocker initialized');
+ } catch (error: any) {
+ console.warn('Failed to initialize adblocker, continuing without it:', error.message);
+ // Still need to set up the CDP session even if blocker fails
+ this.client = await this.currentPage.context().newCDPSession(this.currentPage);
+ }
};
/**
diff --git a/server/src/browser-management/inputHandlers.ts b/server/src/browser-management/inputHandlers.ts
index 982e18de..bf365053 100644
--- a/server/src/browser-management/inputHandlers.ts
+++ b/server/src/browser-management/inputHandlers.ts
@@ -260,6 +260,16 @@ const onTimeSelection = async (data: { selector: string, value: string }) => {
await handleWrapper(handleTimeSelection, data);
}
+const handleDateTimeLocalSelection = async (generator: WorkflowGenerator, page: Page, data: { selector: string, value: string }) => {
+ await generator.onDateTimeLocalSelection(page, data);
+ logger.log('debug', `DateTime Local value ${data.value} selected`);
+}
+
+const onDateTimeLocalSelection = async (data: { selector: string, value: string }) => {
+ logger.log('debug', 'Handling datetime-local selection event emitted from client');
+ await handleWrapper(handleDateTimeLocalSelection, data);
+}
+
/**
* A wrapper function for handling the keyup event.
* @param keyboardInput - the keyboard input of the keyup event
@@ -418,6 +428,7 @@ const registerInputHandlers = (socket: Socket) => {
socket.on("input:date", onDateSelection);
socket.on("input:dropdown", onDropdownSelection);
socket.on("input:time", onTimeSelection);
+ socket.on("input:datetime-local", onDateTimeLocalSelection);
socket.on("action", onGenerateAction);
};
diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts
index cc3d879b..084edb4b 100644
--- a/server/src/routes/auth.ts
+++ b/server/src/routes/auth.ts
@@ -52,7 +52,7 @@ router.post("/register", async (req, res) => {
userId: user.id,
registeredAt: new Date().toISOString(),
});
- console.log(`User registered - ${user.email}`);
+ console.log(`User registered`);
res.json(user);
} catch (error: any) {
console.log(`Could not register user - ${error}`);
diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts
index 9ff4922e..609541de 100644
--- a/server/src/workflow-management/classes/Generator.ts
+++ b/server/src/workflow-management/classes/Generator.ts
@@ -315,6 +315,26 @@ export class WorkflowGenerator {
await this.addPairToWorkflowAndNotifyClient(pair, page);
};
+ public onDateTimeLocalSelection = async (page: Page, data: { selector: string, value: string }) => {
+ const { selector, value } = data;
+
+ try {
+ await page.fill(selector, value);
+ } catch (error) {
+ console.error("Failed to fill datetime-local value:", error);
+ }
+
+ const pair: WhereWhatPair = {
+ where: { url: this.getBestUrl(page.url()) },
+ what: [{
+ action: 'fill',
+ args: [selector, value],
+ }],
+ };
+
+ await this.addPairToWorkflowAndNotifyClient(pair, page);
+ };
+
/**
* Generates a pair for the click event.
* @param coordinates The coordinates of the click event.
@@ -390,6 +410,16 @@ export class WorkflowGenerator {
return;
}
+ const isDateTimeLocal = elementInfo?.tagName === 'INPUT' && elementInfo?.attributes?.type === 'datetime-local';
+
+ if (isDateTimeLocal) {
+ this.socket.emit('showDateTimePicker', {
+ coordinates,
+ selector
+ });
+ return;
+ }
+
//const element = await getElementMouseIsOver(page, coordinates);
//logger.log('debug', `Element: ${JSON.stringify(element, null, 2)}`);
if (selector) {
diff --git a/server/src/workflow-management/scheduler/index.ts b/server/src/workflow-management/scheduler/index.ts
index 169b0061..ade7d969 100644
--- a/server/src/workflow-management/scheduler/index.ts
+++ b/server/src/workflow-management/scheduler/index.ts
@@ -73,7 +73,7 @@ async function createWorkflowAndStoreMetadata(id: string, userId: string) {
} catch (e) {
const { message } = e as Error;
logger.log('info', `Error while scheduling a run with id: ${id}`);
- console.log(message);
+ console.log(`Error while scheduling a run with id: ${id}:`, message);
return {
success: false,
error: message,
diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts
index 9367631f..e6337049 100644
--- a/server/src/workflow-management/selector.ts
+++ b/server/src/workflow-management/selector.ts
@@ -223,24 +223,44 @@ export const getElementInformation = async (
if (originalEl) {
let element = originalEl;
- while (element.parentElement) {
- const parentRect = element.parentElement.getBoundingClientRect();
- const childRect = element.getBoundingClientRect();
+ if (element.tagName === 'TD' || element.tagName === 'TH') {
+ const tableParent = element.closest('table');
+ if (tableParent) {
+ element = tableParent;
+ }
+ }
- const fullyContained =
- parentRect.left <= childRect.left &&
- parentRect.right >= childRect.right &&
- parentRect.top <= childRect.top &&
- parentRect.bottom >= childRect.bottom;
+ if (element.tagName !== 'TABLE') {
+ while (element.parentElement) {
+ if (element.tagName.toLowerCase() === 'body' ||
+ element.tagName.toLowerCase() === 'html') {
+ break;
+ }
- const significantOverlap =
- (childRect.width * childRect.height) /
- (parentRect.width * parentRect.height) > 0.5;
+ const parentRect = element.parentElement.getBoundingClientRect();
+ const childRect = element.getBoundingClientRect();
- if (fullyContained && significantOverlap) {
- element = element.parentElement;
- } else {
- break;
+ const fullyContained =
+ parentRect.left <= childRect.left &&
+ parentRect.right >= childRect.right &&
+ parentRect.top <= childRect.top &&
+ parentRect.bottom >= childRect.bottom;
+
+ const significantOverlap =
+ (childRect.width * childRect.height) /
+ (parentRect.width * parentRect.height) > 0.5;
+
+ if (fullyContained && significantOverlap) {
+ const nextParent = element.parentElement;
+ if (nextParent.tagName.toLowerCase() !== 'body' &&
+ nextParent.tagName.toLowerCase() !== 'html') {
+ element = nextParent;
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
}
}
@@ -512,24 +532,44 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector
if (originalEl) {
let element = originalEl;
- while (element.parentElement) {
- const parentRect = element.parentElement.getBoundingClientRect();
- const childRect = element.getBoundingClientRect();
+ if (element.tagName === 'TD' || element.tagName === 'TH') {
+ const tableParent = element.closest('table');
+ if (tableParent) {
+ element = tableParent;
+ }
+ }
- const fullyContained =
- parentRect.left <= childRect.left &&
- parentRect.right >= childRect.right &&
- parentRect.top <= childRect.top &&
- parentRect.bottom >= childRect.bottom;
+ if (element.tagName !== 'TABLE') {
+ while (element.parentElement) {
+ if (element.tagName.toLowerCase() === 'body' ||
+ element.tagName.toLowerCase() === 'html') {
+ break;
+ }
- const significantOverlap =
- (childRect.width * childRect.height) /
- (parentRect.width * parentRect.height) > 0.5;
+ const parentRect = element.parentElement.getBoundingClientRect();
+ const childRect = element.getBoundingClientRect();
- if (fullyContained && significantOverlap) {
- element = element.parentElement;
- } else {
- break;
+ const fullyContained =
+ parentRect.left <= childRect.left &&
+ parentRect.right >= childRect.right &&
+ parentRect.top <= childRect.top &&
+ parentRect.bottom >= childRect.bottom;
+
+ const significantOverlap =
+ (childRect.width * childRect.height) /
+ (parentRect.width * parentRect.height) > 0.5;
+
+ if (fullyContained && significantOverlap) {
+ const nextParent = element.parentElement;
+ if (nextParent.tagName.toLowerCase() !== 'body' &&
+ nextParent.tagName.toLowerCase() !== 'html') {
+ element = nextParent;
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
}
}
@@ -1386,6 +1426,13 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates
function getNonUniqueSelector(element: HTMLElement): string {
let selector = element.tagName.toLowerCase();
+ if (selector === 'td' && element.parentElement) {
+ // Find position among td siblings
+ const siblings = Array.from(element.parentElement.children);
+ const position = siblings.indexOf(element) + 1;
+ return `${selector}:nth-child(${position})`;
+ }
+
if (element.className) {
const classes = element.className.split(/\s+/).filter((cls: string) => Boolean(cls));
if (classes.length > 0) {
@@ -1474,25 +1521,45 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates
let element = originalEl;
+ if (element.tagName === 'TD' || element.tagName === 'TH') {
+ const tableParent = element.closest('table');
+ if (tableParent) {
+ element = tableParent;
+ }
+ }
+
// if (listSelector === '') {
- while (element.parentElement) {
- const parentRect = element.parentElement.getBoundingClientRect();
- const childRect = element.getBoundingClientRect();
+ if (element.tagName !== 'TABLE') {
+ while (element.parentElement) {
+ if (element.tagName.toLowerCase() === 'body' ||
+ element.tagName.toLowerCase() === 'html') {
+ break;
+ }
- const fullyContained =
- parentRect.left <= childRect.left &&
- parentRect.right >= childRect.right &&
- parentRect.top <= childRect.top &&
- parentRect.bottom >= childRect.bottom;
+ const parentRect = element.parentElement.getBoundingClientRect();
+ const childRect = element.getBoundingClientRect();
- const significantOverlap =
- (childRect.width * childRect.height) /
- (parentRect.width * parentRect.height) > 0.5;
+ const fullyContained =
+ parentRect.left <= childRect.left &&
+ parentRect.right >= childRect.right &&
+ parentRect.top <= childRect.top &&
+ parentRect.bottom >= childRect.bottom;
- if (fullyContained && significantOverlap) {
- element = element.parentElement;
- } else {
- break;
+ const significantOverlap =
+ (childRect.width * childRect.height) /
+ (parentRect.width * parentRect.height) > 0.5;
+
+ if (fullyContained && significantOverlap) {
+ const nextParent = element.parentElement;
+ if (nextParent.tagName.toLowerCase() !== 'body' &&
+ nextParent.tagName.toLowerCase() !== 'html') {
+ element = nextParent;
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
}
}
// }
@@ -1504,7 +1571,6 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates
}, coordinates);
return selectors || { generalSelector: '' };
} else {
- console.log(`NON UNIQUE: MODE 2`)
const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => {
const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => {
// First, get the element at the specified coordinates in the main document
@@ -1555,6 +1621,12 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates
function getNonUniqueSelector(element: HTMLElement): string {
let selector = element.tagName.toLowerCase();
+ if (selector === 'td' && element.parentElement) {
+ const siblings = Array.from(element.parentElement.children);
+ const position = siblings.indexOf(element) + 1;
+ return `${selector}:nth-child(${position})`;
+ }
+
if (element.className) {
const classes = element.className.split(/\s+/).filter((cls: string) => Boolean(cls));
if (classes.length > 0) {
@@ -1664,6 +1736,12 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro
function getNonUniqueSelector(element: HTMLElement): string {
let selector = element.tagName.toLowerCase();
+ if (selector === 'td' && element.parentElement) {
+ const siblings = Array.from(element.parentElement.children);
+ const position = siblings.indexOf(element) + 1;
+ return `${selector}:nth-child(${position})`;
+ }
+
const className = typeof element.className === 'string' ? element.className : '';
if (className) {
const classes = className.split(/\s+/).filter((cls: string) => Boolean(cls));
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/api/storage.ts b/src/api/storage.ts
index 4b2f4e80..18c793c0 100644
--- a/src/api/storage.ts
+++ b/src/api/storage.ts
@@ -5,11 +5,6 @@ import { ScheduleSettings } from "../components/molecules/ScheduleSettings";
import { CreateRunResponse, ScheduleRunResponse } from "../pages/MainPage";
import { apiUrl } from "../apiConfig";
-
-
-
-
-
export const getStoredRecordings = async (): Promise => {
try {
const response = await axios.get(`${apiUrl}/storage/recordings`);
@@ -82,11 +77,7 @@ export const getStoredRecording = async (id: string) => {
}
}
-
-
export const checkRunsForRecording = async (id: string): Promise => {
-
-
try {
const response = await axios.get(`${apiUrl}/storage/recordings/${id}/runs`);
@@ -99,32 +90,26 @@ export const checkRunsForRecording = async (id: string): Promise => {
}
};
-
export const deleteRecordingFromStorage = async (id: string): Promise => {
-
const hasRuns = await checkRunsForRecording(id);
-
+
if (hasRuns) {
-
+
return false;
}
try {
const response = await axios.delete(`${apiUrl}/storage/recordings/${id}`);
if (response.status === 200) {
-
+
return true;
} else {
throw new Error(`Couldn't delete stored recording ${id}`);
}
} catch (error: any) {
console.log(error);
-
+
return false;
}
-
-
-
-
};
export const deleteRunFromStorage = async (id: string): Promise => {
@@ -159,7 +144,7 @@ export const createRunForStoredRecording = async (id: string, settings: RunSetti
try {
const response = await axios.put(
`${apiUrl}/storage/runs/${id}`,
- { ...settings });
+ { ...settings });
if (response.status === 200) {
return response.data;
} else {
diff --git a/src/api/workflow.ts b/src/api/workflow.ts
index 03b677b1..40ac0d99 100644
--- a/src/api/workflow.ts
+++ b/src/api/workflow.ts
@@ -3,7 +3,7 @@ import { emptyWorkflow } from "../shared/constants";
import { default as axios, AxiosResponse } from "axios";
import { apiUrl } from "../apiConfig";
-export const getActiveWorkflow = async(id: string) : Promise => {
+export const getActiveWorkflow = async (id: string): Promise => {
try {
const response = await axios.get(`${apiUrl}/workflow/${id}`)
if (response.status === 200) {
@@ -11,13 +11,13 @@ export const getActiveWorkflow = async(id: string) : Promise => {
} else {
throw new Error('Something went wrong when fetching a recorded workflow');
}
- } catch(error: any) {
+ } catch (error: any) {
console.log(error);
return emptyWorkflow;
}
};
-export const getParamsOfActiveWorkflow = async(id: string) : Promise => {
+export const getParamsOfActiveWorkflow = async (id: string): Promise => {
try {
const response = await axios.get(`${apiUrl}/workflow/params/${id}`)
if (response.status === 200) {
@@ -25,15 +25,15 @@ export const getParamsOfActiveWorkflow = async(id: string) : Promise => {
+export const deletePair = async (index: number): Promise => {
try {
- const response = await axios.delete(`${apiUrl}/workflow/pair/${index}`);
+ const response = await axios.delete(`${apiUrl}/workflow/pair/${index}`);
if (response.status === 200) {
return response.data;
} else {
@@ -45,11 +45,11 @@ export const deletePair = async(index: number): Promise => {
}
};
-export const AddPair = async(index: number, pair: WhereWhatPair): Promise => {
+export const AddPair = async (index: number, pair: WhereWhatPair): Promise => {
try {
const response = await axios.post(`${apiUrl}/workflow/pair/${index}`, {
pair,
- }, {headers: {'Content-Type': 'application/json'}});
+ }, { headers: { 'Content-Type': 'application/json' } });
if (response.status === 200) {
return response.data;
} else {
@@ -61,11 +61,11 @@ export const AddPair = async(index: number, pair: WhereWhatPair): Promise => {
+export const UpdatePair = async (index: number, pair: WhereWhatPair): Promise => {
try {
const response = await axios.put(`${apiUrl}/workflow/pair/${index}`, {
pair,
- }, {headers: {'Content-Type': 'application/json'}});
+ }, { headers: { 'Content-Type': 'application/json' } });
if (response.status === 200) {
return response.data;
} else {
diff --git a/src/components/atoms/DateTimeLocalPicker.tsx b/src/components/atoms/DateTimeLocalPicker.tsx
new file mode 100644
index 00000000..dc62a79b
--- /dev/null
+++ b/src/components/atoms/DateTimeLocalPicker.tsx
@@ -0,0 +1,74 @@
+import React, { useState } from 'react';
+import { useSocketStore } from '../../context/socket';
+import { Coordinates } from './canvas';
+
+interface DateTimeLocalPickerProps {
+ coordinates: Coordinates;
+ selector: string;
+ onClose: () => void;
+}
+
+const DateTimeLocalPicker: React.FC = ({ coordinates, selector, onClose }) => {
+ const { socket } = useSocketStore();
+ const [selectedDateTime, setSelectedDateTime] = useState('');
+
+ const handleDateTimeChange = (e: React.ChangeEvent) => {
+ setSelectedDateTime(e.target.value);
+ };
+
+ const handleConfirm = () => {
+ if (socket && selectedDateTime) {
+ socket.emit('input:datetime-local', {
+ selector,
+ value: selectedDateTime
+ });
+ onClose();
+ }
+ };
+
+ return (
+
+
+
+
+
+ Cancel
+
+
+ Confirm
+
+
+
+
+ );
+};
+
+export default DateTimeLocalPicker;
\ No newline at end of file
diff --git a/src/components/atoms/canvas.tsx b/src/components/atoms/canvas.tsx
index 77128a65..e71a4d93 100644
--- a/src/components/atoms/canvas.tsx
+++ b/src/components/atoms/canvas.tsx
@@ -6,6 +6,7 @@ import { useActionContext } from '../../context/browserActions';
import DatePicker from './DatePicker';
import Dropdown from './Dropdown';
import TimePicker from './TimePicker';
+import DateTimeLocalPicker from './DateTimeLocalPicker';
interface CreateRefCallback {
(ref: React.RefObject): void;
@@ -55,6 +56,11 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
selector: string;
} | null>(null);
+ const [dateTimeLocalInfo, setDateTimeLocalInfo] = React.useState<{
+ coordinates: Coordinates;
+ selector: string;
+ } | null>(null);
+
const notifyLastAction = (action: string) => {
if (lastAction !== action) {
setLastAction(action);
@@ -91,9 +97,15 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
setTimePickerInfo(info);
});
+ socket.on('showDateTimePicker', (info: {coordinates: Coordinates, selector: string}) => {
+ setDateTimeLocalInfo(info);
+ });
+
return () => {
socket.off('showDatePicker');
socket.off('showDropdown');
+ socket.off('showTimePicker');
+ socket.off('showDateTimePicker');
};
}
}, [socket]);
@@ -222,6 +234,13 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
onClose={() => setTimePickerInfo(null)}
/>
)}
+ {dateTimeLocalInfo && (
+ setDateTimeLocalInfo(null)}
+ />
+ )}