+

+
+
- )}
-
-
-

-
-
+
+
+ {user ? (
+
+ {!isRecording ? (
+ <>
+
+
+
+
+
+
+ {user.email}
+
+
+
+ >
+ ) : (
+ <>
+
+
+ {t("discard")}
+
+
+ >
+ )}
+
+
+
+
+
+
- {
- user ? (
-
- {!isRecording ? (
- <>
-
-
-
- {latestVersion === null ? (
- Checking for updates...
- ) : currentVersion === latestVersion ? (
-
- 🎉 You're up to date!
-
- ) : (
- <>
-
- A new version is available: {latestVersion}. Upgrade to the latest version for bug fixes, enhancements and new features!
-
- View all the new updates
- {' '}here.
-
-
-
-
-
- {tab === 0 && (
-
-
- Run the commands below
- # pull latest changes
-
- git pull origin master
-
-
- # install dependencies
-
- npm install
-
-
- # start maxun
-
- npm run start
-
-
- )}
- {tab === 1 && (
-
-
- Run the commands below
- # pull latest docker images
-
- docker-compose pull
-
-
- # start maxun
-
- docker-compose up -d
-
-
- )}
- >
- )}
-
-
-
-
-
- {user.email}
-
-
- >
- ) : (
- <>
-
-
- Discard
-
-
- >
- )}
-
- ) : ""
- }
-
- >
+ ) : (
+ <>
+ {t("language")}
+
+
>
+ )}
+
+
+
+
+
);
};
const NavBarWrapper = styled.div`
grid-area: navbar;
background-color: white;
- padding:5px;
+ padding: 5px;
display: flex;
justify-content: space-between;
border-bottom: 1px solid #e0e0e0;
diff --git a/src/components/molecules/RecordingsTable.tsx b/src/components/molecules/RecordingsTable.tsx
index 651d3677..92bf572a 100644
--- a/src/components/molecules/RecordingsTable.tsx
+++ b/src/components/molecules/RecordingsTable.tsx
@@ -1,4 +1,5 @@
import * as React from 'react';
+import { useTranslation } from 'react-i18next';
import Paper from '@mui/material/Paper';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
@@ -19,6 +20,7 @@ import { useNavigate } from 'react-router-dom';
import { stopRecording } from "../../api/recording";
import { GenericModal } from '../atoms/GenericModal';
+
/** TODO:
* 1. allow editing existing robot after persisting browser steps
*/
@@ -31,30 +33,9 @@ interface Column {
format?: (value: string) => string;
}
-const columns: readonly Column[] = [
- { id: 'interpret', label: 'Run', minWidth: 80 },
- { id: 'name', label: 'Name', minWidth: 80 },
- {
- id: 'schedule',
- label: 'Schedule',
- minWidth: 80,
- },
- {
- id: 'integrate',
- label: 'Integrate',
- minWidth: 80,
- },
- {
- id: 'settings',
- label: 'Settings',
- minWidth: 80,
- },
- {
- id: 'options',
- label: 'Options',
- minWidth: 80,
- },
-];
+
+
+
interface Data {
id: string;
@@ -76,12 +57,38 @@ interface RecordingsTableProps {
}
export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot }: RecordingsTableProps) => {
+ const {t} = useTranslation();
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10);
const [rows, setRows] = React.useState
([]);
const [isModalOpen, setModalOpen] = React.useState(false);
const [searchTerm, setSearchTerm] = React.useState('');
+ const columns: readonly Column[] = [
+ { id: 'interpret', label: t('recordingtable.run'), minWidth: 80 },
+ { id: 'name', label: t('recordingtable.name'), minWidth: 80 },
+ {
+ id: 'schedule',
+ label: t('recordingtable.schedule'),
+ minWidth: 80,
+ },
+ {
+ id: 'integrate',
+ label: t('recordingtable.integrate'),
+ minWidth: 80,
+ },
+ {
+ id: 'settings',
+ label: t('recordingtable.settings'),
+ minWidth: 80,
+ },
+ {
+ id: 'options',
+ label: t('recordingtable.options'),
+ minWidth: 80,
+ },
+ ];
+
const { notify, setRecordings, browserId, setBrowserId, recordingUrl, setRecordingUrl, recordingName, setRecordingName, recordingId, setRecordingId } = useGlobalInfoStore();
const navigate = useNavigate();
@@ -151,16 +158,17 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
row.name.toLowerCase().includes(searchTerm.toLowerCase())
);
+
return (
- My Robots
+ {t('recordingtable.heading')}
- Create Robot
+ {t('recordingtable.new')}
@@ -297,9 +305,9 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
/>
setModalOpen(false)} modalStyle={modalStyle}>
- Enter URL To Extract Data
+ {t('recordingtable.modal.title')}
- Start Training Robot
+ {t('recordingtable.modal.button')}
@@ -397,6 +405,8 @@ const OptionsButton = ({ handleEdit, handleDelete, handleDuplicate }: OptionsBut
setAnchorEl(null);
};
+ const {t} = useTranslation();
+
return (
<>
- Edit
-
-
+
+
+
+
>
);
diff --git a/src/components/molecules/RunsTable.tsx b/src/components/molecules/RunsTable.tsx
index 669cecd6..41dd047a 100644
--- a/src/components/molecules/RunsTable.tsx
+++ b/src/components/molecules/RunsTable.tsx
@@ -1,4 +1,6 @@
import * as React from 'react';
+import { useEffect, useState } from "react";
+import { useTranslation } from 'react-i18next';
import Paper from '@mui/material/Paper';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
@@ -7,14 +9,24 @@ import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TablePagination from '@mui/material/TablePagination';
import TableRow from '@mui/material/TableRow';
-import { useEffect, useState } from "react";
+import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField } from '@mui/material';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import SearchIcon from '@mui/icons-material/Search';
+
import { useGlobalInfoStore } from "../../context/globalInfo";
import { getStoredRuns } from "../../api/storage";
import { RunSettings } from "./RunSettings";
import { CollapsibleRow } from "./ColapsibleRow";
-import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField } from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import SearchIcon from '@mui/icons-material/Search';
+
+// Export columns before the component
+export const columns: readonly Column[] = [
+ { id: 'runStatus', label: 'Status', minWidth: 80 },
+ { id: 'name', label: 'Name', minWidth: 80 },
+ { id: 'startedAt', label: 'Started At', minWidth: 80 },
+ { id: 'finishedAt', label: 'Finished At', minWidth: 80 },
+ { id: 'settings', label: 'Settings', minWidth: 80 },
+ { id: 'delete', label: 'Delete', minWidth: 80 },
+];
interface Column {
id: 'runStatus' | 'name' | 'startedAt' | 'finishedAt' | 'delete' | 'settings';
@@ -24,16 +36,7 @@ interface Column {
format?: (value: string) => string;
}
-export const columns: readonly Column[] = [
- { id: 'runStatus', label: 'Status', minWidth: 80 },
- { id: 'name', label: 'Robot Name', minWidth: 80 },
- { id: 'startedAt', label: 'Started at', minWidth: 80 },
- { id: 'finishedAt', label: 'Finished at', minWidth: 80 },
- { id: 'settings', label: 'Settings', minWidth: 80 },
- { id: 'delete', label: 'Delete', minWidth: 80 },
-];
-
-export interface Data {
+interface Data {
id: number;
status: string;
name: string;
@@ -58,15 +61,25 @@ interface RunsTableProps {
runningRecordingName: string;
}
-export const RunsTable = (
- { currentInterpretationLog, abortRunHandler, runId, runningRecordingName }: RunsTableProps) => {
+export const RunsTable: React.FC = ({
+ currentInterpretationLog,
+ abortRunHandler,
+ runId,
+ runningRecordingName
+}) => {
+ const { t } = useTranslation();
+
+ // Update column labels using translation if needed
+ const translatedColumns = columns.map(column => ({
+ ...column,
+ label: t(`runstable.${column.id}`, column.label)
+ }));
+
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [rows, setRows] = useState([]);
-
const [searchTerm, setSearchTerm] = useState('');
-
const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore();
const handleChangePage = (event: unknown, newPage: number) => {
@@ -86,16 +99,13 @@ export const RunsTable = (
const fetchRuns = async () => {
const runs = await getStoredRuns();
if (runs) {
- const parsedRows: Data[] = [];
- runs.map((run: any, index) => {
- parsedRows.push({
- id: index,
- ...run,
- });
- });
+ const parsedRows: Data[] = runs.map((run: any, index: number) => ({
+ id: index,
+ ...run,
+ }));
setRows(parsedRows);
} else {
- notify('error', 'No runs found. Please try again.')
+ notify('error', 'No runs found. Please try again.');
}
};
@@ -104,7 +114,7 @@ export const RunsTable = (
fetchRuns();
setRerenderRuns(false);
}
- }, [rerenderRuns]);
+ }, [rerenderRuns, rows.length, setRerenderRuns]);
const handleDelete = () => {
setRows([]);
@@ -112,7 +122,6 @@ export const RunsTable = (
fetchRuns();
};
-
// Filter rows based on search term
const filteredRows = rows.filter((row) =>
row.name.toLowerCase().includes(searchTerm.toLowerCase())
@@ -120,7 +129,6 @@ export const RunsTable = (
// Group filtered rows by robot meta id
const groupedRows = filteredRows.reduce((acc, row) => {
-
if (!acc[row.robotMetaId]) {
acc[row.robotMetaId] = [];
}
@@ -132,11 +140,11 @@ export const RunsTable = (
- All Runs
+ {t('runstable.runs', 'Runs')}
(
}>
-
{data[data.length - 1].name}
-
- {columns.map((column) => (
+ {translatedColumns.map((column) => (
);
-};
+};
\ No newline at end of file
diff --git a/src/components/organisms/MainMenu.tsx b/src/components/organisms/MainMenu.tsx
index dadb6731..4143ae9f 100644
--- a/src/components/organisms/MainMenu.tsx
+++ b/src/components/organisms/MainMenu.tsx
@@ -5,6 +5,9 @@ import Box from '@mui/material/Box';
import { Paper, Button } from "@mui/material";
import { AutoAwesome, FormatListBulleted, VpnKey, Usb, CloudQueue, Code } from "@mui/icons-material";
import { apiUrl } from "../../apiConfig";
+import { useTranslation } from 'react-i18next';
+import i18n from '../../i18n';
+
interface MainMenuProps {
value: string;
@@ -12,6 +15,7 @@ interface MainMenuProps {
}
export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenuProps) => {
+ const {t} = useTranslation();
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
handleChangeContent(newValue);
@@ -47,7 +51,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
fontSize: 'medium',
}}
value="recordings"
- label="Robots"
+ label={t('mainmenu.recordings')}
icon={}
iconPosition="start"
/>
@@ -58,7 +62,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
fontSize: 'medium',
}}
value="runs"
- label="Runs"
+ label={t('mainmenu.runs')}
icon={}
iconPosition="start"
/>
@@ -69,7 +73,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
fontSize: 'medium',
}}
value="proxy"
- label="Proxy"
+ label={t('mainmenu.proxy')}
icon={}
iconPosition="start"
/>
@@ -80,7 +84,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
fontSize: 'medium',
}}
value="apikey"
- label="API Key"
+ label={t('mainmenu.apikey')}
icon={}
iconPosition="start"
/>
@@ -88,10 +92,10 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
}>
- Website To API
+ {t('mainmenu.apidocs')}
}>
- Join Maxun Cloud
+ {t('mainmenu.feedback')}
diff --git a/src/i18n.ts b/src/i18n.ts
new file mode 100644
index 00000000..c5e84364
--- /dev/null
+++ b/src/i18n.ts
@@ -0,0 +1,22 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import Backend from 'i18next-http-backend';
+import LanguageDetector from 'i18next-browser-languagedetector';
+
+i18n
+ .use(Backend)
+ .use(LanguageDetector)
+ .use(initReactI18next)
+ .init({
+ fallbackLng: 'en',
+ debug: import.meta.env.DEV,
+ supportedLngs: ['en', 'es', 'ja', 'zh','de'],
+ interpolation: {
+ escapeValue: false, // React already escapes
+ },
+ backend: {
+ loadPath: '/locales/{{lng}}.json',
+ },
+ });
+
+export default i18n;
\ No newline at end of file
diff --git a/src/index.tsx b/src/index.tsx
index 8c14f60a..96f914ff 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
import './index.css';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
+import i18n from "./i18n"
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx
index 87f90b53..3c8e08c4 100644
--- a/src/pages/Login.tsx
+++ b/src/pages/Login.tsx
@@ -1,134 +1,142 @@
-import axios from "axios";
-import { useState, useContext, useEffect, FormEvent } from "react";
-import { useNavigate, Link } from "react-router-dom";
-import { AuthContext } from "../context/auth";
-import { Box, Typography, TextField, Button, CircularProgress, Grid } from "@mui/material";
-import { useGlobalInfoStore } from "../context/globalInfo";
+import axios from "axios";
+import { useState, useContext, useEffect, FormEvent } from "react";
+import { useNavigate, Link } from "react-router-dom";
+import { AuthContext } from "../context/auth";
+import { Box, Typography, TextField, Button, CircularProgress, Grid } from "@mui/material";
+import { useGlobalInfoStore } from "../context/globalInfo";
import { apiUrl } from "../apiConfig";
+import { useTranslation } from 'react-i18next';
+import i18n from '../i18n';
const Login = () => {
- const [form, setForm] = useState({
- email: "",
- password: "",
- });
- const [loading, setLoading] = useState(false);
- const { notify } = useGlobalInfoStore();
- const { email, password } = form;
+ const { t } = useTranslation();
+ console.log(i18n)
+ console.log(t)
+ const [form, setForm] = useState({
+ email: "",
+ password: "",
+ });
+ const [loading, setLoading] = useState(false);
+ const { notify } = useGlobalInfoStore();
+ const { email, password } = form;
- const { state, dispatch } = useContext(AuthContext);
- const { user } = state;
+ const { state, dispatch } = useContext(AuthContext);
+ const { user } = state;
- const navigate = useNavigate();
+ const navigate = useNavigate();
- useEffect(() => {
- if (user) {
- navigate("/");
- }
- }, [user, navigate]);
+ useEffect(() => {
+ if (user) {
+ navigate("/");
+ }
+ }, [user, navigate]);
- const handleChange = (e: any) => {
- const { name, value } = e.target;
- setForm({ ...form, [name]: value });
- };
+ const handleChange = (e: any) => {
+ const { name, value } = e.target;
+ setForm({ ...form, [name]: value });
+ };
- const submitForm = async (e: any) => {
- e.preventDefault();
- setLoading(true);
- try {
- const { data } = await axios.post(`${apiUrl}/auth/login`, {
- email,
- password,
- });
- dispatch({ type: "LOGIN", payload: data });
- notify("success", "Welcome to Maxun!");
- window.localStorage.setItem("user", JSON.stringify(data));
- navigate("/");
- } catch (err) {
- notify("error", "Login Failed. Please try again.");
- setLoading(false);
- }
- };
+ const submitForm = async (e: any) => {
+ e.preventDefault();
+ setLoading(true);
+ try {
+ const { data } = await axios.post(`${apiUrl}/auth/login`, {
+ email,
+ password,
+ });
+ dispatch({ type: "LOGIN", payload: data });
+ notify("success", t('login.welcome_notification')); // Translated notification
+ window.localStorage.setItem("user", JSON.stringify(data));
+ navigate("/");
+ } catch (err) {
+ notify("error", t('login.error_notification')); // Translated error
+ setLoading(false);
+ }
+ };
- return (
-
-
-
-
-
- Welcome Back!
-
-
-
-
-
- Don’t have an account?{" "}
-
- Register
-
-
-
-
-
- );
+ // Language switcher function
+
+
+ return (
+
+ {/* Language Switcher Buttons */}
+
+
+
+
+ {t('login.title')}
+
+
+
+
+
+ {t('login.register_prompt')}{" "}
+
+ {t('login.register_link')}
+
+
+
+
+ );
};
-export default Login;
+export default Login;
\ No newline at end of file
diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx
index c64de4ae..b1d2428f 100644
--- a/src/pages/Register.tsx
+++ b/src/pages/Register.tsx
@@ -5,8 +5,13 @@ import { AuthContext } from "../context/auth";
import { Box, Typography, TextField, Button, CircularProgress } from "@mui/material";
import { useGlobalInfoStore } from "../context/globalInfo";
import { apiUrl } from "../apiConfig";
+import { useTranslation } from 'react-i18next';
+import i18n from '../i18n';
+
+
const Register = () => {
+ const {t} = useTranslation();
const [form, setForm] = useState({
email: "",
password: "",
@@ -40,11 +45,13 @@ const Register = () => {
password,
});
dispatch({ type: "LOGIN", payload: data });
- notify("success", "Registration Successful!");
+ notify("success", t('register.welcome_notification'));
window.localStorage.setItem("user", JSON.stringify(data));
navigate("/");
} catch (error:any) {
- notify("error", `Registration Failed. Please try again. ${error.response.data}`);
+
+ notify("error", error.response.data || t('register.error_notification'));
+
setLoading(false);
}
};
@@ -78,11 +85,11 @@ const Register = () => {
>
- Create an Account
+ {t('register.title')}
{
/>
{
Loading
>
) : (
- "Register"
+ t('register.button')
)}
- Already have an account?{" "}
+ {t('register.register_prompt')}{" "}
- Login
+
+ {t('register.login_link')}