langs
This commit is contained in:
@@ -36,6 +36,9 @@
|
|||||||
"fortawesome": "^0.0.1-security",
|
"fortawesome": "^0.0.1-security",
|
||||||
"google-auth-library": "^9.14.1",
|
"google-auth-library": "^9.14.1",
|
||||||
"googleapis": "^144.0.0",
|
"googleapis": "^144.0.0",
|
||||||
|
"i18next": "^24.0.2",
|
||||||
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
|
"i18next-http-backend": "^3.0.1",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
"joi": "^17.6.0",
|
"joi": "^17.6.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
@@ -56,6 +59,7 @@
|
|||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
"react-highlight": "0.15.0",
|
"react-highlight": "0.15.0",
|
||||||
|
"react-i18next": "^15.1.3",
|
||||||
"react-router-dom": "^6.26.1",
|
"react-router-dom": "^6.26.1",
|
||||||
"react-simple-code-editor": "^0.11.2",
|
"react-simple-code-editor": "^0.11.2",
|
||||||
"react-transition-group": "^4.4.2",
|
"react-transition-group": "^4.4.2",
|
||||||
|
|||||||
0
public/locales/ar.json
Normal file
0
public/locales/ar.json
Normal file
13
public/locales/en.json
Normal file
13
public/locales/en.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
}
|
||||||
13
public/locales/es.json
Normal file
13
public/locales/es.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
public/locales/ja.json
Normal file
13
public/locales/ja.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"name": "Maxun",
|
||||||
|
"version": "beta"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"title": "おかえりなさい!",
|
||||||
|
"email": "メールアドレス",
|
||||||
|
"password": "パスワード",
|
||||||
|
"button": "ログイン",
|
||||||
|
"register_prompt": "アカウントをお持ちでない方は、新規登録"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
public/locales/zh.json
Normal file
13
public/locales/zh.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"name": "Maxun",
|
||||||
|
"version": "beta"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"title": "欢迎回来!",
|
||||||
|
"email": "电子邮件",
|
||||||
|
"password": "密码",
|
||||||
|
"button": "登录",
|
||||||
|
"register_prompt": "没有账号?注册"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/App.tsx
19
src/App.tsx
@@ -1,8 +1,10 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from "react-router-dom";
|
||||||
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
||||||
import { GlobalInfoProvider } from "./context/globalInfo";
|
import { GlobalInfoProvider } from "./context/globalInfo";
|
||||||
import { PageWrapper } from "./pages/PageWrappper";
|
import { PageWrapper } from "./pages/PageWrappper";
|
||||||
|
import i18n from "./i18n";
|
||||||
|
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
@@ -20,14 +22,14 @@ const theme = createTheme({
|
|||||||
},
|
},
|
||||||
containedPrimary: {
|
containedPrimary: {
|
||||||
// Styles for 'contained' variant with 'primary' color
|
// Styles for 'contained' variant with 'primary' color
|
||||||
'&:hover': {
|
"&:hover": {
|
||||||
backgroundColor: "#ff66d9",
|
backgroundColor: "#ff66d9",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
outlined: {
|
outlined: {
|
||||||
// Apply white background for all 'outlined' variant buttons
|
// Apply white background for all 'outlined' variant buttons
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: "#ffffff",
|
||||||
'&:hover': {
|
"&:hover": {
|
||||||
backgroundColor: "#f0f0f0", // Optional lighter background on hover
|
backgroundColor: "#f0f0f0", // Optional lighter background on hover
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -36,7 +38,7 @@ const theme = createTheme({
|
|||||||
MuiLink: {
|
MuiLink: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
'&:hover': {
|
"&:hover": {
|
||||||
color: "#ff00c3",
|
color: "#ff00c3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -63,7 +65,7 @@ const theme = createTheme({
|
|||||||
standardInfo: {
|
standardInfo: {
|
||||||
backgroundColor: "#fce1f4",
|
backgroundColor: "#fce1f4",
|
||||||
color: "#ff00c3",
|
color: "#ff00c3",
|
||||||
'& .MuiAlert-icon': {
|
"& .MuiAlert-icon": {
|
||||||
color: "#ff00c3",
|
color: "#ff00c3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -72,7 +74,7 @@ const theme = createTheme({
|
|||||||
MuiAlertTitle: {
|
MuiAlertTitle: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
'& .MuiAlert-icon': {
|
"& .MuiAlert-icon": {
|
||||||
color: "#ffffff",
|
color: "#ffffff",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -81,15 +83,16 @@ const theme = createTheme({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
|
|
||||||
<GlobalInfoProvider>
|
<GlobalInfoProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/*" element={<PageWrapper />} />
|
<Route path="/*" element={<PageWrapper />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</GlobalInfoProvider>
|
</GlobalInfoProvider>
|
||||||
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,103 @@
|
|||||||
import React, { useState, useContext } from 'react';
|
import React, { useState, useContext } from "react";
|
||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { stopRecording } from "../../api/recording";
|
import { stopRecording } from "../../api/recording";
|
||||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||||
import { IconButton, Menu, MenuItem, Typography, Avatar, Chip, } from "@mui/material";
|
import { IconButton, Menu, MenuItem, Typography, Chip } from "@mui/material";
|
||||||
import { AccountCircle, Logout, Clear } from "@mui/icons-material";
|
import { AccountCircle, Logout, Clear } from "@mui/icons-material";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import { AuthContext } from '../../context/auth';
|
import { AuthContext } from "../../context/auth";
|
||||||
import { SaveRecording } from '../molecules/SaveRecording';
|
import { SaveRecording } from "../molecules/SaveRecording";
|
||||||
import DiscordIcon from '../atoms/DiscordIcon';
|
import DiscordIcon from "../atoms/DiscordIcon";
|
||||||
import { apiUrl } from '../../apiConfig';
|
import { apiUrl } from "../../apiConfig";
|
||||||
import MaxunLogo from "../../assets/maxunlogo.png";
|
import MaxunLogo from "../../assets/maxunlogo.png";
|
||||||
|
import { useTranslation } from "react-i18next"; // Import useTranslation hook
|
||||||
|
|
||||||
interface NavBarProps {
|
interface NavBarProps {
|
||||||
recordingName: string;
|
recordingName: string;
|
||||||
isRecording: boolean;
|
isRecording: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) => {
|
export const NavBar: React.FC<NavBarProps> = ({
|
||||||
const { notify, browserId, setBrowserId, recordingUrl } = useGlobalInfoStore();
|
recordingName,
|
||||||
|
isRecording,
|
||||||
|
}) => {
|
||||||
|
const { notify, browserId, setBrowserId } = useGlobalInfoStore();
|
||||||
const { state, dispatch } = useContext(AuthContext);
|
const { state, dispatch } = useContext(AuthContext);
|
||||||
const { user } = state;
|
const { user } = state;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { t, i18n } = useTranslation(); // Get translation function and i18n methods
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const [langAnchorEl, setLangAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLangMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setLangAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
const handleMenuClose = () => {
|
const handleMenuClose = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
|
setLangAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
dispatch({ type: 'LOGOUT' });
|
dispatch({ type: "LOGOUT" });
|
||||||
window.localStorage.removeItem('user');
|
window.localStorage.removeItem("user");
|
||||||
const { data } = await axios.get(`${apiUrl}/auth/logout`);
|
const { data } = await axios.get(`${apiUrl}/auth/logout`);
|
||||||
notify('success', data.message);
|
notify("success", data.message);
|
||||||
navigate('/login');
|
navigate("/login");
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToMainMenu = async () => {
|
const goToMainMenu = async () => {
|
||||||
if (browserId) {
|
if (browserId) {
|
||||||
await stopRecording(browserId);
|
await stopRecording(browserId);
|
||||||
notify('warning', 'Current Recording was terminated');
|
notify("warning", "Current Recording was terminated");
|
||||||
setBrowserId(null);
|
setBrowserId(null);
|
||||||
}
|
}
|
||||||
navigate('/');
|
navigate("/");
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeLanguage = (lang: string) => {
|
||||||
|
i18n.changeLanguage(lang); // Change language dynamically
|
||||||
|
localStorage.setItem("language", lang); // Persist language to localStorage
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavBarWrapper>
|
<NavBarWrapper>
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
justifyContent: 'flex-start',
|
display: "flex",
|
||||||
}}>
|
justifyContent: "flex-start",
|
||||||
<img src={MaxunLogo} width={45} height={40} style={{ borderRadius: '5px', margin: '5px 0px 5px 15px' }} />
|
}}
|
||||||
<div style={{ padding: '11px' }}><ProjectName>Maxun</ProjectName></div>
|
>
|
||||||
<Chip label="beta" color="primary" variant="outlined" sx={{ marginTop: '10px' }} />
|
<img
|
||||||
|
src={MaxunLogo}
|
||||||
|
width={45}
|
||||||
|
height={40}
|
||||||
|
style={{ borderRadius: "5px", margin: "5px 0px 5px 15px" }}
|
||||||
|
/>
|
||||||
|
<div style={{ padding: "11px" }}>
|
||||||
|
<ProjectName>Maxun</ProjectName>
|
||||||
</div>
|
</div>
|
||||||
{
|
<Chip
|
||||||
user ? (
|
label="beta"
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ marginTop: "10px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{user ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{!isRecording ? (
|
{!isRecording ? (
|
||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -71,25 +106,35 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
borderRadius: '5px',
|
borderRadius: "5px",
|
||||||
padding: '8px',
|
padding: "8px",
|
||||||
marginRight: '30px',
|
marginRight: "30px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DiscordIcon sx={{ marginRight: '5px' }} />
|
<DiscordIcon sx={{ marginRight: "5px" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<iframe src="https://ghbtns.com/github-btn.html?user=getmaxun&repo=maxun&type=star&count=true&size=large" frameBorder="0" scrolling="0" width="170" height="30" title="GitHub"></iframe>
|
<iframe
|
||||||
<IconButton onClick={handleMenuOpen} sx={{
|
src="https://ghbtns.com/github-btn.html?user=getmaxun&repo=maxun&type=star&count=true&size=large"
|
||||||
display: 'flex',
|
frameBorder="0"
|
||||||
alignItems: 'center',
|
scrolling="0"
|
||||||
borderRadius: '5px',
|
width="170"
|
||||||
padding: '8px',
|
height="30"
|
||||||
marginRight: '10px',
|
title="GitHub"
|
||||||
'&:hover': { backgroundColor: 'white', color: '#ff00c3' }
|
></iframe>
|
||||||
}}>
|
<IconButton
|
||||||
<AccountCircle sx={{ marginRight: '5px' }} />
|
onClick={handleMenuOpen}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: "5px",
|
||||||
|
padding: "8px",
|
||||||
|
marginRight: "10px",
|
||||||
|
"&:hover": { backgroundColor: "white", color: "#ff00c3" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AccountCircle sx={{ marginRight: "5px" }} />
|
||||||
<Typography variant="body1">{user.email}</Typography>
|
<Typography variant="body1">{user.email}</Typography>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Menu
|
<Menu
|
||||||
@@ -97,38 +142,181 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
|
|||||||
open={Boolean(anchorEl)}
|
open={Boolean(anchorEl)}
|
||||||
onClose={handleMenuClose}
|
onClose={handleMenuClose}
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
vertical: 'bottom',
|
vertical: "bottom",
|
||||||
horizontal: 'right',
|
horizontal: "right",
|
||||||
}}
|
}}
|
||||||
transformOrigin={{
|
transformOrigin={{
|
||||||
vertical: 'top',
|
vertical: "top",
|
||||||
horizontal: 'right',
|
horizontal: "right",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem onClick={() => { handleMenuClose(); logout(); }}>
|
<MenuItem
|
||||||
<Logout sx={{ marginRight: '5px' }} /> Logout
|
onClick={() => {
|
||||||
|
handleMenuClose();
|
||||||
|
logout();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Logout sx={{ marginRight: "5px" }} /> {t("logout")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
{/* Language dropdown */}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<IconButton onClick={goToMainMenu} sx={{
|
<IconButton
|
||||||
borderRadius: '5px',
|
onClick={goToMainMenu}
|
||||||
padding: '8px',
|
sx={{
|
||||||
background: 'red',
|
borderRadius: "5px",
|
||||||
color: 'white',
|
padding: "8px",
|
||||||
marginRight: '10px',
|
background: "red",
|
||||||
'&:hover': { color: 'white', backgroundColor: 'red' }
|
color: "white",
|
||||||
}}>
|
marginRight: "10px",
|
||||||
<Clear sx={{ marginRight: '5px' }} />
|
"&:hover": { color: "white", backgroundColor: "red" },
|
||||||
Discard
|
}}
|
||||||
|
>
|
||||||
|
<Clear sx={{ marginRight: "5px" }} />
|
||||||
|
{t("discard")}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<SaveRecording fileName={recordingName} />
|
<SaveRecording fileName={recordingName} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<IconButton
|
||||||
|
onClick={handleLangMenuOpen}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: "5px",
|
||||||
|
padding: "8px",
|
||||||
|
marginRight: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">{t("language")}</Typography>
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
anchorEl={langAnchorEl}
|
||||||
|
open={Boolean(langAnchorEl)}
|
||||||
|
onClose={handleMenuClose}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "right",
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "right",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
changeLanguage("en");
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
changeLanguage("es");
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Español
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
changeLanguage("ja");
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
日本語
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
changeLanguage("ar");
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
العربية
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
changeLanguage("zh");
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
中文
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
) : ""
|
) : (
|
||||||
}
|
<><IconButton
|
||||||
|
onClick={handleLangMenuOpen}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: "5px",
|
||||||
|
padding: "8px",
|
||||||
|
marginRight: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">{t("language")}</Typography>
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
anchorEl={langAnchorEl}
|
||||||
|
open={Boolean(langAnchorEl)}
|
||||||
|
onClose={handleMenuClose}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "right",
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "right",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
changeLanguage("en");
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
changeLanguage("es");
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Español
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
changeLanguage("ja");
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
日本語
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
changeLanguage("ar");
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
العربية
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
changeLanguage("zh");
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
中文
|
||||||
|
</MenuItem>
|
||||||
|
</Menu></>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
</NavBarWrapper>
|
</NavBarWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
22
src/i18n.ts
Normal file
22
src/i18n.ts
Normal file
@@ -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', 'ar'],
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false, // React already escapes
|
||||||
|
},
|
||||||
|
backend: {
|
||||||
|
loadPath: '/locales/{{lng}}.json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import i18n from "./i18n"
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement
|
document.getElementById('root') as HTMLElement
|
||||||
|
|||||||
@@ -5,8 +5,13 @@ import { AuthContext } from "../context/auth";
|
|||||||
import { Box, Typography, TextField, Button, CircularProgress, Grid } from "@mui/material";
|
import { Box, Typography, TextField, Button, CircularProgress, Grid } from "@mui/material";
|
||||||
import { useGlobalInfoStore } from "../context/globalInfo";
|
import { useGlobalInfoStore } from "../context/globalInfo";
|
||||||
import { apiUrl } from "../apiConfig";
|
import { apiUrl } from "../apiConfig";
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import i18n from '../i18n'; // Add this import
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
console.log(i18n) // Add translation hook
|
||||||
|
console.log(t)
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
@@ -40,15 +45,20 @@ const Login = () => {
|
|||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
dispatch({ type: "LOGIN", payload: data });
|
dispatch({ type: "LOGIN", payload: data });
|
||||||
notify("success", "Welcome to Maxun!");
|
notify("success", t('login.welcome_notification')); // Translated notification
|
||||||
window.localStorage.setItem("user", JSON.stringify(data));
|
window.localStorage.setItem("user", JSON.stringify(data));
|
||||||
navigate("/");
|
navigate("/");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notify("error", "Login Failed. Please try again.");
|
notify("error", t('login.error_notification')); // Translated error
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Language switcher function
|
||||||
|
const changeLanguage = (lng: string) => {
|
||||||
|
i18n.changeLanguage(lng);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -60,6 +70,7 @@ const Login = () => {
|
|||||||
padding: 4,
|
padding: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Language Switcher Buttons */}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
@@ -79,11 +90,11 @@ const Login = () => {
|
|||||||
>
|
>
|
||||||
<img src="../src/assets/maxunlogo.png" alt="logo" height={55} width={60} style={{ marginBottom: 20, borderRadius: "20%", alignItems: "center" }} />
|
<img src="../src/assets/maxunlogo.png" alt="logo" height={55} width={60} style={{ marginBottom: 20, borderRadius: "20%", alignItems: "center" }} />
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
Welcome Back!
|
{t('login.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Email"
|
label={t('login.email')}
|
||||||
name="email"
|
name="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@@ -93,7 +104,7 @@ const Login = () => {
|
|||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Password"
|
label={t('login.password')}
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
@@ -113,21 +124,20 @@ const Login = () => {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
<CircularProgress size={20} sx={{ mr: 2 }} />
|
<CircularProgress size={20} sx={{ mr: 2 }} />
|
||||||
Loading
|
{t('login.loading')}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"Login"
|
t('login.button')
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Typography variant="body2" align="center">
|
<Typography variant="body2" align="center">
|
||||||
Don’t have an account?{" "}
|
{t('login.register_prompt')}{" "}
|
||||||
<Link to="/register" style={{ textDecoration: "none", color: "#ff33cc" }}>
|
<Link to="/register" style={{ textDecoration: "none", color: "#ff33cc" }}>
|
||||||
Register
|
{t('login.register_link')}
|
||||||
</Link>
|
</Link>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user