Multi-language/I18n support #22

This commit is contained in:
Proddy
2022-08-24 21:50:19 +02:00
parent 763337db3f
commit 1a4ce643fc
84 changed files with 5506 additions and 4196 deletions

View File

@@ -5,17 +5,21 @@ import { Tab } from '@mui/material';
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
import { useI18nContext } from '../i18n/i18n-react';
import DashboardStatus from './DashboardStatus';
import DashboardData from './DashboardData';
const Dashboard: FC = () => {
useLayoutTitle('Dashboard');
const { routerTab } = useRouterTab();
const { LL } = useI18nContext();
useLayoutTitle(LL.DASHBOARD());
return (
<>
<RouterTabs value={routerTab}>
<Tab value="data" label="Devices &amp; Sensors" />
<Tab value="data" label={LL.DEVICES_SENSORS()} />
<Tab value="status" label="Status" />
</RouterTabs>
<Routes>

View File

@@ -74,12 +74,21 @@ import {
DeviceEntityMask
} from './types';
import { useI18nContext } from '../i18n/i18n-react';
const DashboardData: FC = () => {
const { me } = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const [coreData, setCoreData] = useState<CoreData>({ connected: true, devices: [], active_sensors: 0, analog_enabled: false });
const [coreData, setCoreData] = useState<CoreData>({
connected: true,
devices: [],
active_sensors: 0,
analog_enabled: false
});
const [deviceData, setDeviceData] = useState<DeviceData>({ label: '', data: [] });
const [sensorData, setSensorData] = useState<SensorData>({ sensors: [], analogs: [] });
const [deviceValue, setDeviceValue] = useState<DeviceValue>();
@@ -134,7 +143,7 @@ const DashboardData: FC = () => {
common_theme,
{
Table: `
--data-table-library_grid-template-columns: 40px 100px repeat(1, minmax(0, 1fr)) 80px 40px;
--data-table-library_grid-template-columns: 40px 100px repeat(1, minmax(0, 1fr)) 90px 40px;
`,
BaseRow: `
.td {
@@ -429,15 +438,15 @@ const DashboardData: FC = () => {
devicevalue: deviceValue
});
if (response.status === 204) {
enqueueSnackbar('Write command failed', { variant: 'error' });
enqueueSnackbar(LL.WRITE_COMMAND({ cmd: 'failed' }), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Write access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Write command sent', { variant: 'success' });
enqueueSnackbar(LL.WRITE_COMMAND({ cmd: 'send' }), { variant: 'success' });
}
setDeviceValue(undefined);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem writing value'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
refreshData();
setDeviceValue(undefined);
@@ -449,7 +458,7 @@ const DashboardData: FC = () => {
if (deviceValue) {
return (
<Dialog open={deviceValue !== undefined} onClose={() => setDeviceValue(undefined)}>
<DialogTitle>{isCmdOnly(deviceValue) ? 'Run Command' : 'Change Value'}</DialogTitle>
<DialogTitle>{isCmdOnly(deviceValue) ? LL.RUN_COMMAND() : LL.CHANGE_VALUE()}</DialogTitle>
<DialogContent dividers>
{deviceValue.l && (
<ValidatedTextField
@@ -491,7 +500,7 @@ const DashboardData: FC = () => {
onClick={() => setDeviceValue(undefined)}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SendIcon />}
@@ -500,7 +509,7 @@ const DashboardData: FC = () => {
onClick={() => sendDeviceValue()}
color="warning"
>
Send
{LL.SEND()}
</Button>
</DialogActions>
</Dialog>
@@ -521,15 +530,15 @@ const DashboardData: FC = () => {
offset: sensor.o
});
if (response.status === 204) {
enqueueSnackbar('Sensor change failed', { variant: 'error' });
enqueueSnackbar(LL.TEMP_SENSOR({ cmd: 'change failed' }), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Sensor updated', { variant: 'success' });
enqueueSnackbar(LL.TEMP_SENSOR({ cmd: 'removed' }), { variant: 'success' });
}
setSensor(undefined);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem updating sensor'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setSensor(undefined);
fetchSensorData();
@@ -581,7 +590,7 @@ const DashboardData: FC = () => {
onClick={() => setSensor(undefined)}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SaveIcon />}
@@ -590,7 +599,7 @@ const DashboardData: FC = () => {
onClick={() => sendSensor()}
color="warning"
>
Save
{LL.SAVE()}
</Button>
</DialogActions>
</Dialog>
@@ -640,17 +649,20 @@ const DashboardData: FC = () => {
const renderCoreData = () => (
<IconContext.Provider value={{ color: 'lightblue', size: '24', style: { verticalAlign: 'middle' } }}>
{!coreData.connected && <MessageBox my={2} level="error" message="EMSbus disconnected, check settings and board profile" />}
{coreData.connected && coreData.devices.length === 0 && <MessageBox my={2} level="warning" message="Scanning for EMS devices..." />}
{!coreData.connected && <MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />}
{coreData.connected && coreData.devices.length === 0 && (
<MessageBox my={2} level="warning" message={LL.EMS_BUS_SCANNING()} />
)}
<Table data={{ nodes: coreData.devices }} select={device_select} theme={device_theme} layout={{ custom: true }}>
{(tableList: any) => (
<>
<Header>
<HeaderRow>
<HeaderCell stiff />
<HeaderCell stiff>TYPE</HeaderCell>
<HeaderCell resize>DESCRIPTION</HeaderCell>
<HeaderCell stiff>ENTITIES</HeaderCell>
<HeaderCell stiff>{LL.TYPE()}</HeaderCell>
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
<HeaderCell stiff>{LL.ENTITIES()}</HeaderCell>
<HeaderCell stiff />
</HeaderRow>
</Header>
@@ -676,7 +688,7 @@ const DashboardData: FC = () => {
<DeviceIcon type="Sensor" />
</Cell>
<Cell>Sensors</Cell>
<Cell>Attached EMS-ESP Sensors</Cell>
<Cell>{LL.ATTACHED_SENSORS()}</Cell>
<Cell>{coreData.active_sensors}</Cell>
<Cell>
<IconButton size="small" onClick={() => addAnalogSensor()}>
@@ -723,7 +735,7 @@ const DashboardData: FC = () => {
control={<Checkbox size="small" name="onlyFav" checked={onlyFav} onChange={() => setOnlyFav(!onlyFav)} />}
label={
<span style={{ fontSize: '12px' }}>
only show favorites&nbsp;
{LL.SHOW_FAV()}&nbsp;
<StarIcon color="primary" sx={{ fontSize: 12 }} />
</span>
}
@@ -749,7 +761,7 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(dv_sort.state, 'NAME')}
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
ENTITY NAME
{LL.ENTITY_NAME()}
</Button>
</HeaderCell>
<HeaderCell resize>
@@ -759,7 +771,7 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(dv_sort.state, 'VALUE')}
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
>
VALUE
{LL.VALUE()}
</Button>
</HeaderCell>
<HeaderCell stiff />
@@ -943,14 +955,14 @@ const DashboardData: FC = () => {
});
if (response.status === 204) {
enqueueSnackbar('Analog deletion failed', { variant: 'error' });
enqueueSnackbar(LL.ANALOG_SENSOR({ cmd: 'deletion failed' }), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Analog sensor removed', { variant: 'success' });
enqueueSnackbar(LL.ANALOG_SENSOR({ cmd: 'removed' }), { variant: 'success' });
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem updating analog sensor'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setAnalog(undefined);
fetchSensorData();
@@ -971,14 +983,14 @@ const DashboardData: FC = () => {
});
if (response.status === 204) {
enqueueSnackbar('Analog sensor update failed', { variant: 'error' });
enqueueSnackbar(LL.ANALOG_SENSOR({ cmd: 'update failed' }), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Analog sensor updated', { variant: 'success' });
enqueueSnackbar(LL.ANALOG_SENSOR({ cmd: 'updated' }), { variant: 'success' });
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem updating analog'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setAnalog(undefined);
fetchSensorData();
@@ -1131,7 +1143,7 @@ const DashboardData: FC = () => {
<Grid item>
<ValidatedTextField
name="o"
label="Dutycycle"
label="Duty Cycle"
value={numberValue(analog.o)}
sx={{ width: '20ch' }}
type="number"
@@ -1153,7 +1165,7 @@ const DashboardData: FC = () => {
<DialogActions>
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
<Button startIcon={<RemoveIcon />} variant="outlined" color="error" onClick={() => sendRemoveAnalog()}>
Remove
{LL.REMOVE()}
</Button>
</Box>
<Button
@@ -1162,7 +1174,7 @@ const DashboardData: FC = () => {
onClick={() => setAnalog(undefined)}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SaveIcon />}
@@ -1171,7 +1183,7 @@ const DashboardData: FC = () => {
onClick={() => sendAnalog()}
color="warning"
>
Save
{LL.SAVE()}
</Button>
</DialogActions>
</Dialog>
@@ -1180,7 +1192,7 @@ const DashboardData: FC = () => {
};
return (
<SectionContent title="Device and Sensor Data" titleGutter>
<SectionContent title={LL.DEVICE_SENSOR_DATA()} titleGutter>
{renderCoreData()}
{renderDeviceData()}
{renderDeviceDialog()}
@@ -1191,11 +1203,11 @@ const DashboardData: FC = () => {
{renderAnalogDialog()}
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}>
Refresh
{LL.REFRESH()}
</Button>
{device_select.state.id && device_select.state.id !== 'sensor' && (
<Button startIcon={<DownloadIcon />} variant="outlined" onClick={handleDownloadCsv}>
Export
{LL.EXPORT()}
</Button>
)}
</ButtonRow>

View File

@@ -5,12 +5,16 @@ import { Tab } from '@mui/material';
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
import { useI18nContext } from '../i18n/i18n-react';
import HelpInformation from './HelpInformation';
const Help: FC = () => {
useLayoutTitle('Help');
const { LL } = useI18nContext();
const { routerTab } = useRouterTab();
useLayoutTitle(LL.HELP());
return (
<>
<RouterTabs value={routerTab}>

View File

@@ -6,6 +6,8 @@ import { AuthenticatedContext } from '../contexts/authentication';
import { PROJECT_PATH } from '../api/env';
import { useI18nContext } from '../i18n/i18n-react';
import TuneIcon from '@mui/icons-material/Tune';
import DashboardIcon from '@mui/icons-material/Dashboard';
import LayoutMenuItem from '../components/layout/LayoutMenuItem';
@@ -13,17 +15,18 @@ import InfoIcon from '@mui/icons-material/Info';
const ProjectMenu: FC = () => {
const authenticatedContext = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
return (
<List>
<LayoutMenuItem icon={DashboardIcon} label="Dashboard" to={`/${PROJECT_PATH}/dashboard`} />
<LayoutMenuItem icon={DashboardIcon} label={LL.DASHBOARD()} to={`/${PROJECT_PATH}/dashboard`} />
<LayoutMenuItem
icon={TuneIcon}
label="Settings"
label={LL.SETTINGS()}
to={`/${PROJECT_PATH}/settings`}
disabled={!authenticatedContext.me.admin}
/>
<LayoutMenuItem icon={InfoIcon} label="Help" to={`/${PROJECT_PATH}/help`} />
<LayoutMenuItem icon={InfoIcon} label={LL.HELP()} to={`/${PROJECT_PATH}/help`} />
</List>
);
};

View File

@@ -5,13 +5,17 @@ import { Tab } from '@mui/material';
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
import { useI18nContext } from '../i18n/i18n-react';
import SettingsApplication from './SettingsApplication';
import SettingsCustomization from './SettingsCustomization';
const Settings: FC = () => {
useLayoutTitle('Settings');
const { LL } = useI18nContext();
const { routerTab } = useRouterTab();
useLayoutTitle(LL.SETTINGS());
return (
<>
<RouterTabs value={routerTab}>

View File

@@ -3,7 +3,7 @@ import { ValidateFieldsError } from 'async-validator';
import { useSnackbar } from 'notistack';
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider } from '@mui/material';
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider, InputAdornment } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
@@ -24,6 +24,8 @@ import { numberValue, extractErrorMessage, updateValue, useRest } from '../utils
import * as EMSESP from './api';
import { Settings, BOARD_PROFILES } from './types';
import { useI18nContext } from '../i18n/i18n-react';
export function boardProfileSelectItems() {
return Object.keys(BOARD_PROFILES).map((code) => (
<MenuItem key={code} value={code}>
@@ -38,6 +40,8 @@ const SettingsApplication: FC = () => {
update: EMSESP.writeSettings
});
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const updateFormValue = updateValue(setData);
@@ -116,7 +120,7 @@ const SettingsApplication: FC = () => {
<Box color="warning.main">
<Typography variant="body2">
Select a pre-configured interface board profile from the list below or choose "Custom" to configure your own
hardware settings.
hardware settings
</Typography>
</Box>
<ValidatedTextField
@@ -321,6 +325,26 @@ const SettingsApplication: FC = () => {
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
General Options
</Typography>
<Box
sx={{
'& .MuiTextField-root': { width: '25ch' }
}}
>
<ValidatedTextField
name="locale"
label="Language (for device entities)"
disabled={saving}
value={data.locale}
variant="outlined"
onChange={updateFormValue}
margin="normal"
size="small"
select
>
<MenuItem value="en">English</MenuItem>
<MenuItem value="de">Deutsch</MenuItem>
</ValidatedTextField>
</Box>
{data.led_gpio !== 0 && (
<BlockFormControlLabel
control={<Checkbox checked={data.hide_led} onChange={updateFormValue} name="hide_led" />}
@@ -350,7 +374,7 @@ const SettingsApplication: FC = () => {
/>
<BlockFormControlLabel
control={<Checkbox checked={data.readonly_mode} onChange={updateFormValue} name="readonly_mode" />}
label="Enable Read only mode (blocks all outgoing EMS Tx write commands)"
label="Enable read-only mode (blocks all outgoing EMS Tx Write commands)"
disabled={saving}
/>
<BlockFormControlLabel
@@ -371,11 +395,14 @@ const SettingsApplication: FC = () => {
/>
{data.shower_alert && (
<>
<Grid item xs={2}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="shower_alert_trigger"
label="Trigger Time (minutes)"
label="Trigger Time"
InputProps={{
endAdornment: <InputAdornment position="end">minutes</InputAdornment>
}}
variant="outlined"
value={data.shower_alert_trigger}
type="number"
@@ -383,11 +410,14 @@ const SettingsApplication: FC = () => {
disabled={!data.shower_timer}
/>
</Grid>
<Grid item xs={2}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="shower_alert_coldshot"
label="Cold Shot Time (seconds)"
label="Cold Shot Duration"
InputProps={{
endAdornment: <InputAdornment position="end">seconds</InputAdornment>
}}
variant="outlined"
value={data.shower_alert_coldshot}
type="number"
@@ -413,8 +443,8 @@ const SettingsApplication: FC = () => {
margin="normal"
select
>
<MenuItem value={1}>on/off</MenuItem>
<MenuItem value={2}>ON/OFF</MenuItem>
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
<MenuItem value={3}>true/false</MenuItem>
<MenuItem value={5}>1/0</MenuItem>
</ValidatedTextField>
@@ -430,8 +460,8 @@ const SettingsApplication: FC = () => {
margin="normal"
select
>
<MenuItem value={1}>"on"/"off"</MenuItem>
<MenuItem value={2}>"ON"/"OFF"</MenuItem>
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
<MenuItem value={3}>"true"/"false"</MenuItem>
<MenuItem value={4}>true/false</MenuItem>
<MenuItem value={5}>"1"/"0"</MenuItem>
@@ -487,7 +517,7 @@ const SettingsApplication: FC = () => {
/>
{data.syslog_enabled && (
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={5}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_host"
@@ -500,7 +530,7 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
<Grid item xs={6}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_port"
@@ -514,7 +544,7 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
<Grid item xs={5}>
<Grid item xs={4}>
<ValidatedTextField
name="syslog_level"
label="Log Level"
@@ -534,11 +564,14 @@ const SettingsApplication: FC = () => {
<MenuItem value={9}>ALL</MenuItem>
</ValidatedTextField>
</Grid>
<Grid item xs={6}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_mark_interval"
label="Mark Interval (seconds, 0=off)"
label="Mark Interval"
InputProps={{
endAdornment: <InputAdornment position="end">seconds</InputAdornment>
}}
fullWidth
variant="outlined"
value={data.syslog_mark_interval}

View File

@@ -294,7 +294,7 @@ const SettingsCustomization: FC = () => {
return (
<>
<Box mb={2} color="warning.main">
<Typography variant="body2">Select a device and customize each of its entities using the options:</Typography>
<Typography variant="body2">Select a device and customize the entities using the options:</Typography>
<Typography variant="body2">
<OptionIcon type="favorite" isSet={true} />
=mark as favorite&nbsp;&nbsp;

View File

@@ -1,4 +1,5 @@
export interface Settings {
locale: string;
tx_mode: number;
ems_bus_id: number;
syslog_enabled: boolean;