Merge pull request #182 from aruznieto/i18n

feat: i18n
This commit is contained in:
Aliaksei 2023-10-19 19:52:38 +01:00 committed by GitHub
commit 4a5f8469b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 555 additions and 97 deletions

View file

@ -10,7 +10,7 @@ const loginLimiter = rateLimit({
max: Number(process.env.ZU_LOGIN_LIMIT_ATTEMPTS) || 50, // limit each IP to 50 requests per windowMs
message: {
status: 429,
error: "Too many login attempts, please try again in 15 minutes.",
error: "tooManyAttempts",
},
});

View file

@ -8,12 +8,12 @@ export async function authorize(username, password, callback) {
throw err;
}
const user = users.find({ username: username });
if (!user.value()) return callback(new Error("Invalid username or password")); // If return "user not found" someone can do a user listing
if (!user.value()) return callback(new Error("logInFailed")); // If return "user not found" someone can do a user listing
const verified = await verifyHash(password, user.value()["password_hash"]);
if (verified) {
return callback(null, user.value());
} else {
return callback(new Error("Invalid username or password"));
return callback(new Error("logInFailed"));
}
}

View file

@ -11,11 +11,15 @@
"codemirror": "^5.62.3",
"date-fns": "^2.29.2",
"history": "^5.3.0",
"i18next": "^23.5.1",
"i18next-browser-languagedetector": "^7.1.0",
"i18next-http-backend": "^2.2.2",
"ipaddr.js": "^2.0.1",
"lodash": "^4.17.21",
"react": "^17.0.2",
"react-data-table-component": "^6.11.8",
"react-dom": "^17.0.2",
"react-i18next": "^13.3.0",
"react-is": "^17.0.2",
"react-router-dom": "^5.2.0",
"react-use": "^17.4.0",

View file

@ -0,0 +1,67 @@
{
"flowRules": "Flow Rules",
"createNetwork": "Create A Network",
"createOneNetwork": "Please create at least one network",
"controllerNetworks": "Controller networks",
"network_one": "Network",
"network_other": "Networks",
"controllerAddress": "Network controller address",
"loginToContinue": "Please, Log In to continue",
"zerouiDesc": "ZeroUI - ZeroTier Controller Web UI - is a web user interface for a self-hosted ZeroTier network controller.",
"logIn": "Log In",
"logInToken": "Token Log In",
"cancel": "Cancel",
"management": "Management",
"deleteNetwork": "Delete Network",
"deleteAlert": "This action cannot be undone.",
"deleteNetworkConfirm": "Are you sure you want to delete this network?",
"deleteMemberConfirm": "Are you sure you want to delete this member?",
"delete": "Delete",
"logOut": "Log out",
"advancedFeature": "ADVANCED FEATURE",
"noDevices": "No devices have joined this network. Use the app on your devices to join",
"member_one": "Member",
"member_other": "Members",
"addMemberManually": "Manually Add Member",
"name": "Name",
"description": "Description",
"allowBridging": "Allow Ethernet Bridging",
"noAutoIP": "Do Not Auto-Assign IPs",
"capabilities": "Capabilities",
"noCapDef": "No capabilities defined",
"tags": "Tags",
"noTagDef": "No tags defined",
"authorized": "Authorized",
"address": "Address",
"managedIPs": "Managed IPs",
"lastSeen": "Last seen",
"version": "Version",
"physIp": "Physical IP",
"latency": "Latency",
"settings": "Settings",
"generalSettings": "General settings",
"networkId": "Network ID",
"accessControl": "Access control",
"public": "Public",
"private": "Private",
"managedRoutes": "Managed routes",
"addRoute": "Add route",
"target": "Target",
"via": "Via",
"start": "Start",
"end": "End",
"ipv4AutoAssign": "IPv4 Auto-Assign",
"autoAssignPool": "IPv4 Auto-Assign",
"addIPv4Pool": "Add IPv4 Pool",
"multicastLimit": "Multicast Recipient Limit",
"enableBroadcast": "Enable Broadcast",
"logInFailed": "Invalid username or password",
"tooManyAttempts": "Too many login attempts, please try again in 15 minutes.",
"language": "Language",
"notAuthorized": "You are not authorized. Please Log In.",
"saveChanges": "Save changes",
"optional": "Optional",
"destination": "Destination",
"username": "Username",
"password": "Password"
}

View file

@ -0,0 +1,67 @@
{
"flowRules": "Reglas de flujo",
"createNetwork": "Crear una red",
"createOneNetwork": "Por favor, crea al menos una red",
"controllerNetworks": "Controlador de redes",
"network_one": "Red",
"network_other": "Redes",
"controllerAddress": "Dirección del controlador",
"loginToContinue": "Por favor, inicia sesión para continuar",
"zerouiDesc": "ZeroUI - ZeroTier Controller Web UI - es una interfaz de usuario web para un controlador de red ZeroTier self-hosted.",
"logIn": "Iniciar sesión",
"logInToken": "Iniciar sesión con token",
"cancel": "Cancelar",
"management": "Gestión",
"deleteNetwork": "Borrar red",
"deleteAlert": "Esta acción no puede ser revertida.",
"deleteNetworkConfirm": "¿Seguro que deseas borrar esta red?",
"deleteMemberConfirm": "¿Seguro que deseas borrar este usuario?",
"delete": "Borrar",
"logOut": "Cerrar sesión",
"advancedFeature": "CARACTERÍSTICA AVANZADA",
"noDevices": "Ningún dispositivo se ha unido a esta red. Utilice la aplicación en sus dispositivos para unirse",
"member_one": "Miembro",
"member_other": "Miembros",
"addMemberManually": "Añadir miembro manualmente",
"name": "Nombre",
"description": "Descripción",
"allowBridging": "Permitir puente Ethernet",
"noAutoIP": "No autoasignar IPs",
"capabilities": "Permisos",
"noCapDef": "No hay permisos definidos",
"tags": "Etiquetas",
"noTagDef": "No hay etiquetas definidas",
"authorized": "Autorizado",
"address": "Dirección",
"managedIPs": "IPs asignadas",
"lastSeen": "Visto por última vez",
"version": "Versión",
"physIp": "IP pública",
"latency": "Latencia",
"settings": "Ajustes",
"generalSettings": "Ajustes generales",
"networkId": "ID de red",
"accessControl": "Control de acceso",
"public": "Público",
"private": "Privado",
"managedRoutes": "Rutas gestionadas",
"addRoute": "Añadir ruta",
"target": "Objetivo",
"via": "Vía",
"start": "Inicio",
"end": "Final",
"autoAssignPool": "Rango de IPv4 autoasignables",
"ipv4AutoAssign": "Rangos de IPv4 automáticos",
"addIPv4Pool": "Añadir rango IPv4",
"multicastLimit": "Límite de destinatarios multicast",
"enableBroadcast": "Habilitar broadcast",
"logInFailed": "Nombre de usuario o contraseña incorrecto",
"tooManyAttempts": "Demasiados intentos de inicio de sesión. Vuelvee a intentarlo en 15 minutos",
"language": "Idioma",
"notAuthorized": "No estás autorizado. Por favor, inicia sesión.",
"saveChanges": "Guardar cambios",
"optional": "Opcional",
"destination": "Destino",
"username": "Nombre de usuario",
"password": "Contraseña"
}

View file

@ -8,6 +8,7 @@ import Bar from "./components/Bar";
import Home from "./routes/Home";
import NotFound from "./routes/NotFound";
import Network from "./routes/Network/Network";
import Settings from "./routes/Settings";
function App() {
return (
@ -17,6 +18,7 @@ function App() {
<Switch>
<Route exact path="/" component={Home} />
<Route path="/network/:nwid" component={Network} />
<Route path="/settings" component={Settings} />
<Route path="/404" component={NotFound} />
<Redirect to="/404" />
</Switch>

View file

@ -19,6 +19,8 @@ import MenuIcon from "@material-ui/icons/Menu";
import LogIn from "components/LogIn";
import { useTranslation } from "react-i18next";
function Bar() {
const [loggedIn, setLoggedIn] = useLocalStorage("loggedIn", false);
const [disabledAuth] = useLocalStorage("disableAuth", false);
@ -41,16 +43,18 @@ function Bar() {
history.go(0);
};
const { t, i18n } = useTranslation();
const menuItems = [
// TODO: add settings page
// {
// name: "Settings",
// to: "/settings",
// },
{
name: t("settings"),
to: "/settings",
},
...(!disabledAuth
? [
{
name: "Log out",
name: t("logOut"),
divide: true,
onClick: onLogOutClick,
},
@ -115,7 +119,6 @@ function Bar() {
key={index}
onClick={() => {
closeMenu();
menuItem.onClick();
}}
>

View file

@ -9,6 +9,8 @@ import NetworkButton from "./components/NetworkButton";
import API from "utils/API";
import { generateNetworkConfig } from "utils/NetworkConfig";
import { useTranslation } from "react-i18next";
function HomeLoggedIn() {
const [networks, setNetworks] = useState([]);
@ -30,6 +32,8 @@ function HomeLoggedIn() {
fetchData();
}, []);
const { t, i18n } = useTranslation();
return (
<div className={classes.root}>
<Button
@ -38,19 +42,19 @@ function HomeLoggedIn() {
className={classes.createBtn}
onClick={createNetwork}
>
Create A Network
{t("createNetwork")}
</Button>
<Divider />
<Grid container spacing={3} className={classes.container}>
<Grid item xs={6}>
<Typography variant="h5">Controller networks</Typography>
{networks[0] && "Network controller address"}
<Typography variant="h5">{t("controllerNetworks")}</Typography>
{networks[0] && t("controllerAddress")}
<Box fontWeight="fontWeightBold">
{networks[0] && networks[0]["id"].slice(0, 10)}
</Box>
</Grid>
<Grid item xs="auto">
<Typography>Networks</Typography>
<Typography>{t("network", { count: networks.length })}</Typography>
<Grid item>
{networks[0] ? (
networks.map((network) => (
@ -59,7 +63,7 @@ function HomeLoggedIn() {
</Grid>
))
) : (
<div>Please create at least one network</div>
<div>{t("createOneNetwork")}</div>
)}
</Grid>
</Grid>

View file

@ -3,6 +3,8 @@ import { Grid, Typography } from "@material-ui/core";
import { useLocalStorage } from "react-use";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import axios from "axios";
function HomeLoggedOut() {
@ -29,6 +31,8 @@ function HomeLoggedOut() {
fetchData();
}, [history, setDisableAuth, setLoggedIn, setToken]);
const { t, i18n } = useTranslation();
return (
<Grid
container
@ -42,14 +46,11 @@ function HomeLoggedOut() {
>
<Grid item xs={10}>
<Typography variant="h5">
<span>
ZeroUI - ZeroTier Controller Web UI - is a web user interface for a
self-hosted ZeroTier network controller.
</span>
<span>{t("zerouiDesc")}</span>
</Typography>
<Typography>
<span>Please Log In to continue</span>
<span>{t("loginToContinue")}</span>
</Typography>
</Grid>
</Grid>

View file

@ -12,6 +12,8 @@ import {
DialogTitle,
} from "@material-ui/core";
import { useTranslation } from "react-i18next";
function LogInToken() {
const [open, setOpen] = useState(false);
const [errorText, setErrorText] = useState("");
@ -41,6 +43,8 @@ function LogInToken() {
}
};
const { t, i18n } = useTranslation();
const LogIn = () => {
if (token.length !== 32) {
setErrorText("Token length error");
@ -55,12 +59,12 @@ function LogInToken() {
return (
<div>
<Button onClick={handleClickOpen} color="inherit" variant="outlined">
Token Log In
{t("logInToken")}
</Button>
<Dialog open={open} onClose={handleClose} onKeyPress={handleKeyPress}>
<DialogTitle>Log In</DialogTitle>
<DialogTitle>{t("logIn")}</DialogTitle>
<DialogContent>
<DialogContentText>ADVANCED FEATURE.</DialogContentText>
<DialogContentText>{t("advancedFeature")}</DialogContentText>
<TextField
value={token}
onChange={(e) => {
@ -76,10 +80,10 @@ function LogInToken() {
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
{t("cancel")}
</Button>
<Button onClick={LogIn} color="primary">
Log In
{t("logIn")}
</Button>
</DialogActions>
</Dialog>

View file

@ -13,6 +13,8 @@ import {
import axios from "axios";
import { useTranslation } from "react-i18next";
function LogInUser() {
const [open, setOpen] = useState(false);
const [snackbarOpen, setSnackbarOpen] = useState(false);
@ -72,13 +74,15 @@ function LogInUser() {
});
};
const { t, i18n } = useTranslation();
return (
<>
<Button onClick={handleClickOpen} color="primary" variant="contained">
Log In
{t("logIn")}
</Button>
<Dialog open={open} onClose={handleClose} onKeyPress={handleKeyPress}>
<DialogTitle>Log In</DialogTitle>
<DialogTitle>{t("logIn")}</DialogTitle>
<DialogContent>
<TextField
autoFocus
@ -87,7 +91,7 @@ function LogInUser() {
setUsername(e.target.value);
}}
margin="dense"
label="username"
label={t("username")}
type="username"
fullWidth
/>
@ -97,17 +101,17 @@ function LogInUser() {
setPassword(e.target.value);
}}
margin="dense"
label="password"
label={t("password")}
type="password"
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
{t("cancel")}
</Button>
<Button onClick={LogIn} color="primary">
Log In
{t("logIn")}
</Button>
</DialogActions>
</Dialog>
@ -117,7 +121,7 @@ function LogInUser() {
vertical: "top",
horizontal: "center",
}}
message={error}
message={t(error)}
/>
</>
);

View file

@ -18,6 +18,8 @@ import DeleteIcon from "@material-ui/icons/Delete";
import API from "utils/API";
import { useTranslation } from "react-i18next";
function NetworkManagement() {
const { nwid } = useParams();
const history = useHistory();
@ -42,10 +44,12 @@ function NetworkManagement() {
history.go(0);
};
const { t, i18n } = useTranslation();
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Management</Typography>
<Typography>{t("management")}</Typography>
</AccordionSummary>
<AccordionDetails>
<Button
@ -54,21 +58,19 @@ function NetworkManagement() {
startIcon={<DeleteIcon />}
onClick={handleClickOpen}
>
Delete Network
{t("deleteNetwork")}
</Button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>
{"Are you sure you want to delete this network?"}
</DialogTitle>
<DialogTitle>{t("deleteNetworkConfirm")}</DialogTitle>
<DialogContent>
<DialogContentText>This action cannot be undone.</DialogContentText>
<DialogContentText>{t("deleteAlert")}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
{t("cancel")}
</Button>
<Button onClick={deleteNetwork} color="secondary">
Delete
{t("delete")}
</Button>
</DialogActions>
</Dialog>

View file

@ -21,6 +21,8 @@ import ManagedIP from "./components/ManagedIP";
import MemberName from "./components/MemberName";
import MemberSettings from "./components/MemberSettings";
import { useTranslation } from "react-i18next";
function NetworkMembers({ network }) {
const { nwid } = useParams();
const [members, setMembers] = useState([]);
@ -46,6 +48,8 @@ function NetworkMembers({ network }) {
console.log("Action:", req);
};
const { t, i18n } = useTranslation();
const handleChange =
(member, key1, key2 = null, mode = "text", id = null) =>
(event) => {
@ -67,7 +71,7 @@ function NetworkMembers({ network }) {
const columns = [
{
id: "auth",
name: "Authorized",
name: t("authorized"),
minWidth: "80px",
cell: (row) => (
<Checkbox
@ -79,7 +83,7 @@ function NetworkMembers({ network }) {
},
{
id: "address",
name: "Address",
name: t("address"),
minWidth: "150px",
cell: (row) => (
<Typography variant="body2">{row.config.address}</Typography>
@ -87,19 +91,19 @@ function NetworkMembers({ network }) {
},
{
id: "name",
name: "Name / Description",
name: t("name") + "/" + t("description"),
minWidth: "250px",
cell: (row) => <MemberName member={row} handleChange={handleChange} />,
},
{
id: "ips",
name: "Managed IPs",
name: t("managedIPs"),
minWidth: "220px",
cell: (row) => <ManagedIP member={row} handleChange={handleChange} />,
},
{
id: "status",
name: "Last Seen",
id: "lastSeen",
name: t("lastSeen"),
minWidth: "100px",
cell: (row) =>
row.online === 1 ? (
@ -121,7 +125,7 @@ function NetworkMembers({ network }) {
},
{
id: "physicalip",
name: "Version / Physical IP / Latency",
name: t("version") + " / " + t("physIp") + " / " + t("latency"),
minWidth: "220px",
cell: (row) =>
row.online === 1 ? (
@ -143,7 +147,7 @@ function NetworkMembers({ network }) {
},
{
id: "delete",
name: "",
name: t("settings"),
minWidth: "50px",
right: true,
cell: (row) => (
@ -162,7 +166,7 @@ function NetworkMembers({ network }) {
return (
<Accordion defaultExpanded={true}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Members</Typography>
<Typography>{t("member", { count: members.length })}</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container direction="column" spacing={3}>
@ -188,8 +192,7 @@ function NetworkMembers({ network }) {
}}
>
<Typography variant="h6" style={{ padding: "10%" }}>
No devices have joined this network. Use the app on your
devices to join <b>{nwid}</b>.
{t("noDevices")} <b>{nwid}</b>.
</Typography>
</Grid>
)}

View file

@ -5,6 +5,8 @@ import AddIcon from "@material-ui/icons/Add";
import API from "utils/API";
import { useTranslation } from "react-i18next";
function AddMember({ nwid, callback }) {
const [member, setMember] = useState("");
@ -24,9 +26,11 @@ function AddMember({ nwid, callback }) {
setMember("");
};
const { t, i18n } = useTranslation();
return (
<>
<Typography>Manually Add Member</Typography>
<Typography>{t("addMemberManually")}</Typography>
<List
disablePadding={true}
style={{

View file

@ -12,8 +12,10 @@ import {
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline";
import API from "utils/API";
import { useTranslation } from "react-i18next";
function DeleteMember({ nwid, mid, callback }) {
const { t, i18n } = useTranslation();
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
@ -37,18 +39,16 @@ function DeleteMember({ nwid, mid, callback }) {
<DeleteOutlineIcon color="secondary" style={{ fontSize: 20 }} />
</IconButton>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>
{"Are you sure you want to delete this member?"}
</DialogTitle>
<DialogTitle>{t("deleteMemberConfirm")}</DialogTitle>
<DialogContent>
<DialogContentText>This action cannot be undone.</DialogContentText>
<DialogContentText>{t("deleteAlert")}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
{t("cancel")}
</Button>
<Button onClick={deleteMemberReq} color="secondary">
Delete
{t("delete")}
</Button>
</DialogActions>
</Dialog>

View file

@ -1,12 +1,14 @@
import { Grid, TextField } from "@material-ui/core";
import { useTranslation } from "react-i18next";
function MemberName({ member, handleChange }) {
const { t, i18n } = useTranslation();
return (
<Grid>
<TextField
value={member.name}
onChange={handleChange(member, "name")}
label="Name"
label={t("name")}
variant="filled"
InputLabelProps={{
shrink: true,
@ -15,7 +17,7 @@ function MemberName({ member, handleChange }) {
<TextField
value={member.description}
onChange={handleChange(member, "description")}
label="Description"
label={t("description")}
variant="filled"
InputLabelProps={{
shrink: true,

View file

@ -13,7 +13,10 @@ import BuildIcon from "@material-ui/icons/Build";
import { useState } from "react";
import Tag from "./components/Tag";
import { useTranslation } from "react-i18next";
function MemberSettings({ member, network, handleChange }) {
const { t, i18n } = useTranslation();
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
@ -30,7 +33,9 @@ function MemberSettings({ member, network, handleChange }) {
<BuildIcon style={{ fontSize: 20 }} />
</IconButton>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>{"Member " + member.config.id + " settings"}</DialogTitle>
<DialogTitle>
{t("member") + member.config.id + t("settings")}
</DialogTitle>
<DialogContent>
<Grid item>
<Checkbox
@ -43,7 +48,7 @@ function MemberSettings({ member, network, handleChange }) {
"checkbox"
)}
/>
<span>Allow Ethernet Bridging</span>
<span>{t("allowBridging")}</span>
</Grid>
<Grid item>
<Checkbox
@ -56,17 +61,17 @@ function MemberSettings({ member, network, handleChange }) {
"checkbox"
)}
/>
<span>Do Not Auto-Assign IPs</span>
<span>{t("noAutoIP")}</span>
</Grid>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h6">Capabilities</Typography>
<Typography variant="h6">{t("capabilities")}</Typography>
</Grid>
<Grid item xs={12}>
<Paper style={{ padding: 20 }}>
{Object.entries(network["capabilitiesByName"] || []).length ===
0
? "No capabilities defined"
? t("noCapDef")
: ""}
{Object.entries(network["capabilitiesByName"] || []).map(
([capName, capId]) => (
@ -96,11 +101,11 @@ function MemberSettings({ member, network, handleChange }) {
</Grid>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h6">Tags</Typography>
<Typography variant="h6">{t("tags")}</Typography>
</Grid>
{Object.entries(network["tagsByName"] || []).length === 0 ? (
<Grid item xs={12}>
<Paper style={{ padding: 20 }}>No tags defined</Paper>
<Paper style={{ padding: 20 }}>{t("noTagDef")}</Paper>
</Grid>
) : (
""

View file

@ -17,7 +17,11 @@ import debounce from "lodash/debounce";
import { useState } from "react";
import API from "utils/API";
import { useTranslation, Trans } from "react-i18next";
function NetworkRules({ network, callback }) {
const { t, i18n } = useTranslation();
const [editor, setEditor] = useState(null);
const [flowData, setFlowData] = useState({
rules: [...network.config.rules],
@ -87,12 +91,12 @@ function NetworkRules({ network, callback }) {
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Flow Rules</Typography>
<Typography>{t("flowRules")}</Typography>
</AccordionSummary>
<AccordionDetails>
{/* Important note: value in CodeMirror instance means INITAIL VALUE
or it could be used to replace editor state with the new value.
No need to update on every user character input
No need to update on every user character input Flow Rules
*/}
<CodeMirror
value={network["rulesSource"]}
@ -130,7 +134,7 @@ function NetworkRules({ network, callback }) {
</Typography>
) : (
<Button variant="contained" color="primary" onClick={saveChanges}>
Save Changes
{t("saveChanges")}
</Button>
)}
</Grid>

View file

@ -17,7 +17,10 @@ import IPv4AutoAssign from "./components/IPv4AutoAssign";
import API from "utils/API";
import { parseValue, replaceValue, setValue } from "utils/ChangeHelper";
import { useTranslation } from "react-i18next";
function NetworkSettings({ network, setNetwork }) {
const { t, i18n } = useTranslation();
const sendReq = async (data) => {
try {
const req = await API.post("/network/" + network["config"]["id"], data);
@ -43,12 +46,12 @@ function NetworkSettings({ network, setNetwork }) {
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>General settings</Typography>
<Typography>{t("generalSettings")}</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container direction="column" spacing={3}>
<Grid item>
<Typography>Network ID</Typography>
<Typography>{t("networkId")}</Typography>
<Typography variant="h5">
<span>{network["config"]["id"]}</span>
</Typography>
@ -57,7 +60,7 @@ function NetworkSettings({ network, setNetwork }) {
<TextField
value={network["config"]["name"]}
onChange={handleChange("config", "name")}
label="Name"
label={t("name")}
variant="filled"
InputLabelProps={{
shrink: true,
@ -71,7 +74,7 @@ function NetworkSettings({ network, setNetwork }) {
multiline
minRows={2}
maxRows={Infinity}
label="Description"
label={t("description")}
variant="filled"
InputLabelProps={{
shrink: true,
@ -80,14 +83,14 @@ function NetworkSettings({ network, setNetwork }) {
</Grid>
<Divider />
<Grid item>
<Typography>Access Control</Typography>
<Typography>{t("accessControl")}</Typography>
<Select
native
value={network["config"]["private"]}
onChange={handleChange("config", "private", "json")}
>
<option value={true}>Private</option>
<option value={false}>Public</option>
<option value={1}>{t("private")}</option>
<option value={0}>{t("public")}</option>
</Select>
</Grid>
<Divider />
@ -111,7 +114,7 @@ function NetworkSettings({ network, setNetwork }) {
<Divider />
<Grid item>
<TextField
label="Multicast Recipient Limit"
label={t("multicastLimit")}
type="number"
value={network["config"]["multicastLimit"]}
onChange={handleChange("config", "multicastLimit", "json")}
@ -126,7 +129,7 @@ function NetworkSettings({ network, setNetwork }) {
color="primary"
onChange={handleChange("config", "enableBroadcast", "checkbox")}
/>
<span>Enable Broadcast</span>
<span>{t("enableBroadcast")}</span>
</Grid>
{/* TODO: */}
{/* <Grid item>

View file

@ -18,7 +18,10 @@ import DataTable from "react-data-table-component";
import { addressPool } from "utils/NetworkConfig";
import { getCIDRAddress, validateIP, normilizeIP } from "utils/IP";
import { useTranslation } from "react-i18next";
function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
const { t, i18n } = useTranslation();
const [start, setStart] = useState("");
const [end, setEnd] = useState("");
@ -89,19 +92,19 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
},
{
id: "Start",
name: "Start",
name: t("start"),
cell: (row) => row["ipRangeStart"],
},
{
id: "End",
name: "End",
name: t("end"),
cell: (row) => row["ipRangeEnd"],
},
];
return (
<>
<Typography>IPv4 Auto-Assign</Typography>
<Typography>{t("ipv4AutoAssign")}</Typography>
<div
style={{
padding: "30px",
@ -122,7 +125,7 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
</Grid>
</div>
<Typography style={{ paddingBottom: "10px" }}>
Auto-Assign Pools
{t("autoAssignPool")}
</Typography>
<Box border={1} borderColor="grey.300">
<Grid item style={{ margin: "10px" }}>
@ -132,7 +135,7 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
data={ipAssignmentPools}
/>
<Divider />
<Typography>Add IPv4 Pool</Typography>
<Typography>{t("addIPv4Pool")}</Typography>
<List
style={{
display: "flex",
@ -142,7 +145,7 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
<TextField
value={start}
onChange={handleStartInput}
placeholder={"Start"}
placeholder={t("start")}
/>
<Divider
orientation="vertical"
@ -154,7 +157,7 @@ function IPv4AutoAssign({ ipAssignmentPools, handleChange }) {
<TextField
value={end}
onChange={handleEndInput}
placeholder={"End"}
placeholder={t("end")}
/>
<IconButton
size="small"

View file

@ -16,7 +16,10 @@ import DataTable from "react-data-table-component";
import { validateIP, normilizeIP, validateCIDR } from "utils/IP";
import { useTranslation } from "react-i18next";
function ManagedRoutes({ routes, handleChange }) {
const { t, i18n } = useTranslation();
const [destination, setDestination] = useState("");
const [via, setVia] = useState("");
@ -71,12 +74,12 @@ function ManagedRoutes({ routes, handleChange }) {
},
{
id: "target",
name: "Target",
name: t("target"),
cell: (row) => row["target"],
},
{
id: "via",
name: "via",
name: t("via"),
cell: (row) => (row["via"] ? row["via"] : "(LAN)"),
},
];
@ -84,13 +87,13 @@ function ManagedRoutes({ routes, handleChange }) {
return (
<>
<Typography style={{ paddingBottom: "10px" }}>
Managed Routes ({routes.length + "/128"})
{t("managedRoutes")} ({routes.length + "/128"})
</Typography>
<Box border={1} borderColor="grey.300">
<Grid item style={{ margin: "10px" }}>
<DataTable noHeader={true} columns={columns} data={routes} />
<Divider />
<Typography>Add Routes</Typography>
<Typography>{t("addRoute")}</Typography>
<List
style={{
display: "flex",
@ -100,7 +103,7 @@ function ManagedRoutes({ routes, handleChange }) {
<TextField
value={destination}
onChange={handleDestinationInput}
placeholder={"Destination (CIDR)"}
placeholder={t("destination") + " (CIDR)"}
/>
<Divider
orientation="vertical"
@ -112,7 +115,7 @@ function ManagedRoutes({ routes, handleChange }) {
<TextField
value={via}
onChange={handleViaInput}
placeholder={"Via (Optional)"}
placeholder={t("via") + " (" + t("optional") + ")"}
/>
<IconButton size="small" color="primary" onClick={addRouteReq}>
<AddIcon

View file

@ -0,0 +1,43 @@
import {
Accordion,
AccordionSummary,
AccordionDetails,
Checkbox,
Divider,
Grid,
Typography,
TextField,
Select,
} from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import API from "utils/API";
import { parseValue, replaceValue, setValue } from "utils/ChangeHelper";
import { useTranslation } from "react-i18next";
function Settings() {
const { t, i18n } = useTranslation();
const handleChange = () => (event) => {
i18n.changeLanguage(event.target.value);
};
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{t("language")}</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid item>
<Select native value={i18n.language} onChange={handleChange()}>
<option value={"en"}>English</option>
<option value={"es-ES"}>Español</option>
</Select>
</Grid>
</AccordionDetails>
</Accordion>
);
}
export default Settings;

View file

@ -0,0 +1 @@
export { default } from "./Settings";

34
frontend/src/i18n.js Normal file
View file

@ -0,0 +1,34 @@
import i18n from "i18next";
import languageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import Backend from "i18next-http-backend";
const userLanguage = window.navigator.language;
i18n
.use(languageDetector)
.use(initReactI18next)
.use(Backend)
.init({
compatibilityJSON: "v4",
fallbackLng: "en",
detection: {
order: ["path", "cookie", "localStorage", "htmlTag"],
caches: ["localStorage", "cookie"],
},
debug: true,
interpolation: {
escapeValue: true,
},
react: {
useSuspense: true,
},
supportedLngs: ["en", "es-ES"],
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
ns: ["common"],
defaultNS: "common",
});
export default i18n;

View file

@ -1,13 +1,17 @@
import "./index.css";
import React from "react";
import React, { Suspense } from "react";
import ReactDOM from "react-dom";
import App from "./App";
import "./i18n";
ReactDOM.render(
<React.StrictMode>
<App />
<Suspense fallback={<div>Loading...</div>}>
<App />
</Suspense>
</React.StrictMode>,
document.getElementById("root")
);

View file

@ -11,7 +11,10 @@ import { useLocalStorage } from "react-use";
import API from "utils/API";
import useStyles from "./Network.styles";
import { useTranslation } from "react-i18next";
function Network() {
const { t, i18n } = useTranslation();
const { nwid } = useParams();
const [loggedIn] = useLocalStorage("loggedIn", false);
const [network, setNetwork] = useState({});
@ -42,7 +45,7 @@ function Network() {
<div className={classes.breadcrumbs}>
<Link color="inherit" component={RouterLink} to="/" underline="none">
<ArrowBackIcon className={classes.backIcon}></ArrowBackIcon>
Networks
{t("network", { count: 2 })}
</Link>
</div>
<div className={classes.container}>
@ -73,9 +76,7 @@ function Network() {
}}
>
<Grid item xs={10}>
<Typography variant="h5">
You are not authorized. Please Log In
</Typography>
<Typography variant="h5">{t("notAuthorized")}</Typography>
</Grid>
</Grid>
);

View file

@ -0,0 +1,52 @@
import { Grid, Link, Typography } from "@material-ui/core";
import ArrowBackIcon from "@material-ui/icons/ArrowBack";
import SettingsComponent from "components/Settings";
import { Link as RouterLink } from "react-router-dom";
import { useLocalStorage } from "react-use";
import useStyles from "./Settings.styles";
import { useTranslation } from "react-i18next";
function Settings() {
const { t, i18n } = useTranslation();
const [loggedIn] = useLocalStorage("loggedIn", false);
const classes = useStyles();
if (loggedIn) {
return (
<>
<div className={classes.breadcrumbs}>
<Link color="inherit" component={RouterLink} to="/" underline="none">
<ArrowBackIcon className={classes.backIcon}></ArrowBackIcon>
{t("settings")}
</Link>
</div>
<div className={classes.container}>
<SettingsComponent />
</div>
</>
);
} else {
return (
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justify="center"
style={{
minHeight: "50vh",
}}
>
<Grid item xs={10}>
<Typography variant="h5">{t("notAuthorized")}</Typography>
</Grid>
</Grid>
);
}
}
export default Settings;

View file

@ -0,0 +1,16 @@
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
backIcon: {
fontSize: 12,
},
container: {
margin: "3%",
},
breadcrumbs: {
paddingTop: "2%",
paddingLeft: "2%",
},
}));
export default useStyles;

View file

@ -0,0 +1 @@
export { default } from "./Settings";

121
yarn.lock
View file

@ -265,6 +265,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.22.5":
version: 7.23.2
resolution: "@babel/runtime@npm:7.23.2"
dependencies:
regenerator-runtime: "npm:^0.14.0"
checksum: abdcbdd590c7e31762e1bdab94dd466823c8bcedd3ff2fde85eeb94dac7cccaef151ac37c428bda7018ededd27c9a82b4dfeb621f978ad934232475a902f8e3a
languageName: node
linkType: hard
"@babel/template@npm:^7.22.15":
version: 7.22.15
resolution: "@babel/template@npm:7.22.15"
@ -2992,6 +3001,15 @@ __metadata:
languageName: node
linkType: hard
"cross-fetch@npm:3.1.6":
version: 3.1.6
resolution: "cross-fetch@npm:3.1.6"
dependencies:
node-fetch: "npm:^2.6.11"
checksum: e08325b813da37f2d5312b3e630af992c35681c1737707b029e8ef1c48ea034bda8b960000fc8bee6e0485e133347198aa6ecccadb530b06c47472f6c76bc27b
languageName: node
linkType: hard
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3":
version: 7.0.3
resolution: "cross-spawn@npm:7.0.3"
@ -4344,11 +4362,15 @@ __metadata:
eslint-plugin-react-hooks: "npm:^4.6.0"
eslint-plugin-react-refresh: "npm:^0.4.3"
history: "npm:^5.3.0"
i18next: "npm:^23.5.1"
i18next-browser-languagedetector: "npm:^7.1.0"
i18next-http-backend: "npm:^2.2.2"
ipaddr.js: "npm:^2.0.1"
lodash: "npm:^4.17.21"
react: "npm:^17.0.2"
react-data-table-component: "npm:^6.11.8"
react-dom: "npm:^17.0.2"
react-i18next: "npm:^13.3.0"
react-is: "npm:^17.0.2"
react-router-dom: "npm:^5.2.0"
react-use: "npm:^17.4.0"
@ -4888,6 +4910,15 @@ __metadata:
languageName: node
linkType: hard
"html-parse-stringify@npm:^3.0.1":
version: 3.0.1
resolution: "html-parse-stringify@npm:3.0.1"
dependencies:
void-elements: "npm:3.1.0"
checksum: 8743b76cc50e46d1956c1ad879d18eb9613b0d2d81e24686d633f9f69bb26b84676f64a926973de793cca479997017a63219278476d617b6c42d68246d7c07fe
languageName: node
linkType: hard
"http-cache-semantics@npm:^4.1.1":
version: 4.1.1
resolution: "http-cache-semantics@npm:4.1.1"
@ -4968,6 +4999,33 @@ __metadata:
languageName: node
linkType: hard
"i18next-browser-languagedetector@npm:^7.1.0":
version: 7.1.0
resolution: "i18next-browser-languagedetector@npm:7.1.0"
dependencies:
"@babel/runtime": "npm:^7.19.4"
checksum: 3b06c8a5df09092cffc0b6637b542bb572e8a25dcba97d0d8a5e5dd7539b90bf00000f3a279654693f4b5908c5fc4d1d4f3766dfb461dacab46be3d071266384
languageName: node
linkType: hard
"i18next-http-backend@npm:^2.2.2":
version: 2.2.2
resolution: "i18next-http-backend@npm:2.2.2"
dependencies:
cross-fetch: "npm:3.1.6"
checksum: dbf09f2f309cb6070e691d0d382ccff3e94d2cfa9f30315dc4c03faa5d7d1f3b408d48c46c766b7e07527ec10c0542fde19240845905314ce0134ac10d6a6adb
languageName: node
linkType: hard
"i18next@npm:^23.5.1":
version: 23.5.1
resolution: "i18next@npm:23.5.1"
dependencies:
"@babel/runtime": "npm:^7.22.5"
checksum: 38e62d582b0f67eb2eee4f079c9cd512246496f2fb970f50a0be26c7c5e6ac5e772de9763ac1943919ecd816b2c0375f4b2071c67b1485a6a980c4d37348408f
languageName: node
linkType: hard
"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24":
version: 0.4.24
resolution: "iconv-lite@npm:0.4.24"
@ -6532,6 +6590,20 @@ __metadata:
languageName: node
linkType: hard
"node-fetch@npm:^2.6.11":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
whatwg-url: "npm:^5.0.0"
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
checksum: b24f8a3dc937f388192e59bcf9d0857d7b6940a2496f328381641cb616efccc9866e89ec43f2ec956bbd6c3d3ee05524ce77fe7b29ccd34692b3a16f237d6676
languageName: node
linkType: hard
"node-gyp@npm:latest":
version: 9.4.0
resolution: "node-gyp@npm:9.4.0"
@ -7288,6 +7360,24 @@ __metadata:
languageName: node
linkType: hard
"react-i18next@npm:^13.3.0":
version: 13.3.0
resolution: "react-i18next@npm:13.3.0"
dependencies:
"@babel/runtime": "npm:^7.22.5"
html-parse-stringify: "npm:^3.0.1"
peerDependencies:
i18next: ">= 23.2.3"
react: ">= 16.8.0"
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
checksum: 2ef46245ba1ba9fca8c43dbe1bcab4ac63ca68e67de7159cb5f93cd7fd10407599bcdf1999f6567091987be7b9aaba77cdb515a70d5ee2b935b985f4c40d6d9d
languageName: node
linkType: hard
"react-is@npm:^16.13.1, react-is@npm:^16.6.0, react-is@npm:^16.7.0":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
@ -8654,6 +8744,13 @@ __metadata:
languageName: node
linkType: hard
"tr46@npm:~0.0.3":
version: 0.0.3
resolution: "tr46@npm:0.0.3"
checksum: 8f1f5aa6cb232f9e1bdc86f485f916b7aa38caee8a778b378ffec0b70d9307873f253f5cbadbe2955ece2ac5c83d0dc14a77513166ccd0a0c7fe197e21396695
languageName: node
linkType: hard
"tree-kill@npm:^1.2.2":
version: 1.2.2
resolution: "tree-kill@npm:1.2.2"
@ -9046,6 +9143,13 @@ __metadata:
languageName: node
linkType: hard
"void-elements@npm:3.1.0":
version: 3.1.0
resolution: "void-elements@npm:3.1.0"
checksum: 0390f818107fa8fce55bb0a5c3f661056001c1d5a2a48c28d582d4d847347c2ab5b7f8272314cac58acf62345126b6b09bea623a185935f6b1c3bbce0dfd7f7f
languageName: node
linkType: hard
"wcwidth@npm:^1.0.1":
version: 1.0.1
resolution: "wcwidth@npm:1.0.1"
@ -9055,6 +9159,23 @@ __metadata:
languageName: node
linkType: hard
"webidl-conversions@npm:^3.0.0":
version: 3.0.1
resolution: "webidl-conversions@npm:3.0.1"
checksum: b65b9f8d6854572a84a5c69615152b63371395f0c5dcd6729c45789052296df54314db2bc3e977df41705eacb8bc79c247cee139a63fa695192f95816ed528ad
languageName: node
linkType: hard
"whatwg-url@npm:^5.0.0":
version: 5.0.0
resolution: "whatwg-url@npm:5.0.0"
dependencies:
tr46: "npm:~0.0.3"
webidl-conversions: "npm:^3.0.0"
checksum: f95adbc1e80820828b45cc671d97da7cd5e4ef9deb426c31bcd5ab00dc7103042291613b3ef3caec0a2335ed09e0d5ed026c940755dbb6d404e2b27f940fdf07
languageName: node
linkType: hard
"which-boxed-primitive@npm:^1.0.2":
version: 1.0.2
resolution: "which-boxed-primitive@npm:1.0.2"