diff --git a/package.json b/package.json index 9fab1c55..866c3dd2 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,13 @@ "fortawesome": "^0.0.1-security", "google-auth-library": "^9.14.1", "googleapis": "^144.0.0", + + "i18next": "^24.0.2", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^3.0.1", + "idcac-playwright": "^0.1.3", + "ioredis": "^5.4.1", "joi": "^17.6.0", "jsonwebtoken": "^9.0.2", @@ -57,6 +63,7 @@ "react": "^18.0.0", "react-dom": "^18.0.0", "react-highlight": "0.15.0", + "react-i18next": "^15.1.3", "react-router-dom": "^6.26.1", "react-simple-code-editor": "^0.11.2", "react-transition-group": "^4.4.2", diff --git a/public/locales/de.json b/public/locales/de.json new file mode 100644 index 00000000..d12b2489 --- /dev/null +++ b/public/locales/de.json @@ -0,0 +1,50 @@ +{ + "login": { + "title": "Willkommen zurück!", + "email": "E-Mail", + "password": "Passwort", + "button": "Einloggen", + "loading": "Lädt", + "register_prompt": "Noch keinen Account?", + "register_link": "Registrieren", + "welcome_notification": "Willkommen bei Maxun!", + "error_notification": "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut." + }, + "register": { + "title": "Konto registrieren", + "email": "E-Mail", + "password": "Passwort", + "button": "Registrieren", + "loading": "Lädt", + "register_prompt": "Bereits ein Konto?", + "login_link": "Einloggen", + "welcome_notification": "Willkommen bei Maxun!", + "error_notification": "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut." + }, + "recordingtable": { + "run": "Ausführen", + "name": "Name", + "schedule": "Zeitplan", + "integrate": "Integrieren", + "settings": "Einstellungen", + "options": "Optionen", + "heading": "Meine Roboter", + "new": "Roboter erstellen", + "modal": { + "title": "Geben Sie die URL ein", + "label": "URL", + "button": "Aufnahme starten" + }, + "edit": "Bearbeiten", + "delete": "Löschen", + "duplicate": "Duplizieren" + }, + "mainmenu": { + "recordings": "Roboter", + "runs": "Ausführungen", + "proxy": "Proxy", + "apikey": "API-Schlüssel", + "feedback": "Maxun Cloud beitreten", + "apidocs": "API-Dokumentation" + } +} diff --git a/public/locales/en.json b/public/locales/en.json new file mode 100644 index 00000000..b6396893 --- /dev/null +++ b/public/locales/en.json @@ -0,0 +1,66 @@ +{ + "login": { + "title": "Welcome Back!", + "email": "Email", + "password": "Password", + "button": "Login", + "loading": "Loading", + "register_prompt": "Don't have an account?", + "register_link": "Register", + "welcome_notification": "Welcome to Maxun!", + "error_notification": "Login Failed. Please try again." + }, + "register": { + "title": "Register Account", + "email": "Email", + "password": "Password", + "button": "Register", + "loading": "Loading", + "register_prompt": "Already have an account?", + "login_link": "Login", + "welcome_notification": "Welcome to Maxun!", + "error_notification": "Registeration Failed. Please try again." + }, + "recordingtable":{ + "run": "Run", + "name": "Name", + "schedule": "Schedule", + "integrate": "Integrate", + "settings": "Settings", + "options": "Options", + "heading":"My Robots", + "new":"Create Robot", + "modal":{ + "title":"Enter the URL", + "label":"URL", + "button":"Start Recording" + }, + "edit":"Edit", + "delete":"Delete", + "duplicate":"Duplicate", + "search":"Search Robots..." + + }, + "mainmenu":{ + "recordings": "Robots", + "runs": "Runs", + "proxy": "Proxy", + "apikey": "API Key", + "feedback":"Join Maxun Cloud", + "apidocs":"API Docs" + + }, + "runstable":{ + "runs":"All Runs", + "runStatus":"Status", + "runName":"Name", + "startedAt":"Started At", + "finishedAt":"Finished At", + "delete":"Delete", + "settings":"Settings", + "search":"Search Runs..." + + + + } + } \ No newline at end of file diff --git a/public/locales/es.json b/public/locales/es.json new file mode 100644 index 00000000..00589622 --- /dev/null +++ b/public/locales/es.json @@ -0,0 +1,13 @@ +{ + "app": { + "name": "Maxun", + "version": "beta" + }, + "login": { + "title": "¡Bienvenido de nuevo!", + "email": "Correo electrónico", + "password": "Contraseña", + "button": "Iniciar sesión", + "register_prompt": "¿No tienes una cuenta? Regístrate" + } + } \ No newline at end of file diff --git a/public/locales/ja.json b/public/locales/ja.json new file mode 100644 index 00000000..881fd131 --- /dev/null +++ b/public/locales/ja.json @@ -0,0 +1,61 @@ +{ + "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": "ロボットを検索..." + }, + "mainmenu": { + "recordings": "ロボット", + "runs": "実行", + "proxy": "プロキシ", + "apikey": "APIキー", + "feedback": "Maxunクラウドに参加する", + "apidocs": "APIドキュメント" + }, + "runstable": { + "runs": "すべての実行", + "runStatus": "ステータス", + "runName": "名前", + "startedAt": "開始日時", + "finishedAt": "終了日時", + "delete": "削除", + "settings": "設定", + "search": "実行を検索..." + } +} diff --git a/public/locales/zh.json b/public/locales/zh.json new file mode 100644 index 00000000..7fa8bb60 --- /dev/null +++ b/public/locales/zh.json @@ -0,0 +1,13 @@ +{ + "app": { + "name": "Maxun", + "version": "beta" + }, + "login": { + "title": "欢迎回来!", + "email": "电子邮件", + "password": "密码", + "button": "登录", + "register_prompt": "没有账号?注册" + } + } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index c37de9ea..02dff134 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,10 @@ -import React from 'react'; -import { Routes, Route } from 'react-router-dom'; +import React from "react"; +import { Routes, Route } from "react-router-dom"; import { ThemeProvider, createTheme } from "@mui/material/styles"; import { GlobalInfoProvider } from "./context/globalInfo"; import { PageWrapper } from "./pages/PageWrappper"; +import i18n from "./i18n"; + const theme = createTheme({ palette: { @@ -20,14 +22,14 @@ const theme = createTheme({ }, containedPrimary: { // Styles for 'contained' variant with 'primary' color - '&:hover': { + "&:hover": { backgroundColor: "#ff66d9", }, }, outlined: { // Apply white background for all 'outlined' variant buttons backgroundColor: "#ffffff", - '&:hover': { + "&:hover": { backgroundColor: "#f0f0f0", // Optional lighter background on hover }, }, @@ -36,7 +38,7 @@ const theme = createTheme({ MuiLink: { styleOverrides: { root: { - '&:hover': { + "&:hover": { color: "#ff00c3", }, }, @@ -63,7 +65,7 @@ const theme = createTheme({ standardInfo: { backgroundColor: "#fce1f4", color: "#ff00c3", - '& .MuiAlert-icon': { + "& .MuiAlert-icon": { color: "#ff00c3", }, }, @@ -72,7 +74,7 @@ const theme = createTheme({ MuiAlertTitle: { styleOverrides: { root: { - '& .MuiAlert-icon': { + "& .MuiAlert-icon": { color: "#ffffff", }, }, @@ -81,15 +83,16 @@ const theme = createTheme({ }, }); - function App() { return ( - - - } /> - - + + + + } /> + + + ); } diff --git a/src/components/molecules/NavBar.tsx b/src/components/molecules/NavBar.tsx index c2f271cf..ead395c5 100644 --- a/src/components/molecules/NavBar.tsx +++ b/src/components/molecules/NavBar.tsx @@ -1,10 +1,24 @@ + + + + + + + + + + + + +import { useTranslation } from "react-i18next"; // Import useTranslation hook + import React, { useState, useContext, useEffect } from 'react'; import axios from 'axios'; import styled from "styled-components"; import { stopRecording } from "../../api/recording"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { IconButton, Menu, MenuItem, Typography, Chip, Button, Modal, Tabs, Tab, Box, Snackbar } from "@mui/material"; -import { AccountCircle, Logout, Clear, YouTube, X, Update, Close } from "@mui/icons-material"; +import { AccountCircle, Logout, Clear, YouTube, X, Update, Close,Language } from "@mui/icons-material"; import { useNavigate } from 'react-router-dom'; import { AuthContext } from '../../context/auth'; import { SaveRecording } from '../molecules/SaveRecording'; @@ -13,18 +27,26 @@ import { apiUrl } from '../../apiConfig'; import MaxunLogo from "../../assets/maxunlogo.png"; import packageJson from "../../../package.json" + interface NavBarProps { recordingName: string; isRecording: boolean; } -export const NavBar: React.FC = ({ recordingName, isRecording }) => { - const { notify, browserId, setBrowserId, recordingUrl } = useGlobalInfoStore(); +export const NavBar: React.FC = ({ + recordingName, + isRecording, +}) => { + const { notify, browserId, setBrowserId } = useGlobalInfoStore(); const { state, dispatch } = useContext(AuthContext); const { user } = state; const navigate = useNavigate(); + const { t, i18n } = useTranslation(); // Get translation function and i18n methods const [anchorEl, setAnchorEl] = useState(null); + + const [langAnchorEl, setLangAnchorEl] = useState(null); + const currentVersion = packageJson.version; const [open, setOpen] = useState(false); @@ -58,29 +80,40 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) => setTab(newValue); }; + const handleMenuOpen = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; + const handleLangMenuOpen = (event: React.MouseEvent) => { + setLangAnchorEl(event.currentTarget); + }; + const handleMenuClose = () => { setAnchorEl(null); + setLangAnchorEl(null); }; const logout = async () => { - dispatch({ type: 'LOGOUT' }); - window.localStorage.removeItem('user'); + dispatch({ type: "LOGOUT" }); + window.localStorage.removeItem("user"); const { data } = await axios.get(`${apiUrl}/auth/logout`); - notify('success', data.message); - navigate('/login'); + notify("success", data.message); + navigate("/login"); }; const goToMainMenu = async () => { if (browserId) { await stopRecording(browserId); - notify('warning', 'Current Recording was terminated'); + notify("warning", "Current Recording was terminated"); setBrowserId(null); } - navigate('/'); + navigate("/"); + }; + + const changeLanguage = (lang: string) => { + i18n.changeLanguage(lang); // Change language dynamically + localStorage.setItem("language", lang); // Persist language to localStorage }; useEffect(() => { @@ -95,228 +128,281 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) => }, []); return ( - <> - {isUpdateAvailable && ( - setIsUpdateAvailable(false)} - message={ - `New version ${latestVersion} available! Click "Upgrade" to update.` - } - action={ - <> - - setIsUpdateAvailable(false)} - style={{ color: 'black' }} - > - - - - } - ContentProps={{ - sx: { - background: "white", - color: "black", - } - }} + + + +
+ +
+ Maxun +
+ - )} - -
- -
Maxun
- + +
+ {user ? ( +
+ {!isRecording ? ( + <> + + + + + + + {user.email} + + + { + handleMenuClose(); + logout(); + }} + > + {t("logout")} + + + + + ) : ( + <> + + + {t("discard")} + + + + )} + + + + + + + { + changeLanguage("en"); + handleMenuClose(); + }} + > + English + + { + changeLanguage("es"); + handleMenuClose(); + }} + > + Español + + { + changeLanguage("ja"); + handleMenuClose(); + }} + > + 日本語 + + { + changeLanguage("ar"); + handleMenuClose(); + }} + > + العربية + + { + changeLanguage("zh"); + handleMenuClose(); + }} + > + 中文 + + { + changeLanguage("de"); + handleMenuClose(); + }} + > + German + + +
- { - 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} - - - { handleMenuClose(); logout(); }}> - Logout - - { - window.open('https://discord.gg/5GbPjBUkws', '_blank'); - }}> - Discord - - { - window.open('https://www.youtube.com/@MaxunOSS/videos?ref=app', '_blank'); - }}> - YouTube - - { - window.open('https://x.com/maxun_io?ref=app', '_blank'); - }}> - Twiiter (X) - - - - ) : ( - <> - - - Discard - - - - )} -
- ) : "" - } -
- + ) : ( + <> + {t("language")} + + + { + changeLanguage("en"); + handleMenuClose(); + }} + > + English + + { + changeLanguage("es"); + handleMenuClose(); + }} + > + Español + + { + changeLanguage("ja"); + handleMenuClose(); + }} + > + 日本語 + + { + changeLanguage("ar"); + handleMenuClose(); + }} + > + العربية + + { + changeLanguage("zh"); + handleMenuClose(); + }} + > + 中文 + + + )} + + + + + ); }; 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 - - { handleDuplicate(); handleClose(); }}> - - - - Duplicate + {t('recordingtable.edit')} + { handleDelete(); handleClose(); }}> - Delete + {t('recordingtable.delete')} + + { handleDuplicate(); handleClose(); }}> + + + + {t('recordingtable.duplicate')} + + ); 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
diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 00000000..c5e84364 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,22 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import Backend from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: 'en', + debug: import.meta.env.DEV, + supportedLngs: ['en', 'es', 'ja', 'zh','de'], + interpolation: { + escapeValue: false, // React already escapes + }, + backend: { + loadPath: '/locales/{{lng}}.json', + }, + }); + +export default i18n; \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 8c14f60a..96f914ff 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'; import './index.css'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; +import i18n from "./i18n" const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 87f90b53..3c8e08c4 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,134 +1,142 @@ -import axios from "axios"; -import { useState, useContext, useEffect, FormEvent } from "react"; -import { useNavigate, Link } from "react-router-dom"; -import { AuthContext } from "../context/auth"; -import { Box, Typography, TextField, Button, CircularProgress, Grid } from "@mui/material"; -import { useGlobalInfoStore } from "../context/globalInfo"; +import axios from "axios"; +import { useState, useContext, useEffect, FormEvent } from "react"; +import { useNavigate, Link } from "react-router-dom"; +import { AuthContext } from "../context/auth"; +import { Box, Typography, TextField, Button, CircularProgress, Grid } from "@mui/material"; +import { useGlobalInfoStore } from "../context/globalInfo"; import { apiUrl } from "../apiConfig"; +import { useTranslation } from 'react-i18next'; +import i18n from '../i18n'; const Login = () => { - const [form, setForm] = useState({ - email: "", - password: "", - }); - const [loading, setLoading] = useState(false); - const { notify } = useGlobalInfoStore(); - const { email, password } = form; + const { t } = useTranslation(); + console.log(i18n) + console.log(t) + const [form, setForm] = useState({ + email: "", + password: "", + }); + const [loading, setLoading] = useState(false); + const { notify } = useGlobalInfoStore(); + const { email, password } = form; - const { state, dispatch } = useContext(AuthContext); - const { user } = state; + const { state, dispatch } = useContext(AuthContext); + const { user } = state; - const navigate = useNavigate(); + const navigate = useNavigate(); - useEffect(() => { - if (user) { - navigate("/"); - } - }, [user, navigate]); + useEffect(() => { + if (user) { + navigate("/"); + } + }, [user, navigate]); - const handleChange = (e: any) => { - const { name, value } = e.target; - setForm({ ...form, [name]: value }); - }; + const handleChange = (e: any) => { + const { name, value } = e.target; + setForm({ ...form, [name]: value }); + }; - const submitForm = async (e: any) => { - e.preventDefault(); - setLoading(true); - try { - const { data } = await axios.post(`${apiUrl}/auth/login`, { - email, - password, - }); - dispatch({ type: "LOGIN", payload: data }); - notify("success", "Welcome to Maxun!"); - window.localStorage.setItem("user", JSON.stringify(data)); - navigate("/"); - } catch (err) { - notify("error", "Login Failed. Please try again."); - setLoading(false); - } - }; + const submitForm = async (e: any) => { + e.preventDefault(); + setLoading(true); + try { + const { data } = await axios.post(`${apiUrl}/auth/login`, { + email, + password, + }); + dispatch({ type: "LOGIN", payload: data }); + notify("success", t('login.welcome_notification')); // Translated notification + window.localStorage.setItem("user", JSON.stringify(data)); + navigate("/"); + } catch (err) { + notify("error", t('login.error_notification')); // Translated error + setLoading(false); + } + }; - return ( - - - - logo - - Welcome Back! - - - - - - Don’t have an account?{" "} - - Register - - - - - - ); + // Language switcher function + + + return ( + + {/* Language Switcher Buttons */} + + + logo + + {t('login.title')} + + + + + + {t('login.register_prompt')}{" "} + + {t('login.register_link')} + + + + + ); }; -export default Login; +export default Login; \ No newline at end of file diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index c64de4ae..b1d2428f 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -5,8 +5,13 @@ import { AuthContext } from "../context/auth"; import { Box, Typography, TextField, Button, CircularProgress } from "@mui/material"; import { useGlobalInfoStore } from "../context/globalInfo"; import { apiUrl } from "../apiConfig"; +import { useTranslation } from 'react-i18next'; +import i18n from '../i18n'; + + const Register = () => { + const {t} = useTranslation(); const [form, setForm] = useState({ email: "", password: "", @@ -40,11 +45,13 @@ const Register = () => { password, }); dispatch({ type: "LOGIN", payload: data }); - notify("success", "Registration Successful!"); + notify("success", t('register.welcome_notification')); window.localStorage.setItem("user", JSON.stringify(data)); navigate("/"); } catch (error:any) { - notify("error", `Registration Failed. Please try again. ${error.response.data}`); + + notify("error", error.response.data || t('register.error_notification')); + setLoading(false); } }; @@ -78,11 +85,11 @@ const Register = () => { > logo - Create an Account + {t('register.title')} { /> { Loading ) : ( - "Register" + t('register.button') )} - Already have an account?{" "} + {t('register.register_prompt')}{" "} - Login + + {t('register.login_link')}