feat: i18n

This commit is contained in:
Andres 2023-10-16 17:34:52 +02:00
parent d65e6fb71a
commit 61fd0e7186
24 changed files with 416 additions and 79 deletions

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: "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)}
{networks[0] && String(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,7 @@ function LogIn() {
<Divider orientation="vertical" />
</>
)}
&nbsp;
<LogInUser />
</>
);

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
@ -104,10 +108,10 @@ function LogInUser() {
</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("ips"),
minWidth: "220px",
cell: (row) => <ManagedIP member={row} handleChange={handleChange} />,
},
{
id: "status",
name: "Last Seen",
name: t("status"),
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("addMemberMan")}</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 = () => {
@ -43,7 +46,7 @@ function MemberSettings({ member, network, handleChange }) {
"checkbox"
)}
/>
<span>Allow Ethernet Bridging</span>
<span>{t("allowBridging")}</span>
</Grid>
<Grid item>
<Checkbox
@ -56,17 +59,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 +99,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"]}

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("netId")}</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("enaBroadcast")}</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",

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

@ -0,0 +1,32 @@
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",
lng: userLanguage || "en",
fallbackLng: "en",
debug: true,
//keySeparator: false, // we use content as keys
interpolation: {
escapeValue: true,
},
react: {
useSuspense: false,
},
supportedLngs: ["en", "es", "es-ES"],
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
ns: ["translation"],
defaultNS: "translation",
});
export default i18n;

View file

@ -5,6 +5,8 @@ import ReactDOM from "react-dom";
import App from "./App";
import "./i18n";
ReactDOM.render(
<React.StrictMode>
<App />