Add localization support to admin status and error messages (#4631)

* Initial plan

* Add localization support to admin form status and error messages

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Format updated files with prettier

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Replace t() with Translation component in admin page JSX

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* update package-lock.json

* Update web/i18n/en/translation.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: gabek <414923+gabek@users.noreply.github.com>
Co-authored-by: Gabe Kangas <gabek@real-ity.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot
2025-10-13 16:29:40 -07:00
committed by GitHub
co-authored by gabek Copilot copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Gabe Kangas
parent fd89c6e8f2
commit 383b80851b
18 changed files with 702 additions and 297 deletions
+6 -1
View File
@@ -1,5 +1,6 @@
import { Popconfirm, Select, Typography } from 'antd'; import { Popconfirm, Select, Typography } from 'antd';
import React, { FC, useContext, useEffect, useState } from 'react'; import React, { FC, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'next-export-i18n';
import { AlertMessageContext } from '../../utils/alert-message-context'; import { AlertMessageContext } from '../../utils/alert-message-context';
import { import {
API_VIDEO_CODEC, API_VIDEO_CODEC,
@@ -13,11 +14,13 @@ import {
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import { Localization } from '../../types/localization';
import { FormStatusIndicator } from './FormStatusIndicator'; import { FormStatusIndicator } from './FormStatusIndicator';
export type CodecSelectorProps = {}; export type CodecSelectorProps = {};
export const CodecSelector: FC<CodecSelectorProps> = () => { export const CodecSelector: FC<CodecSelectorProps> = () => {
const { t } = useTranslation();
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {}; const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { videoCodec, supportedCodecs } = serverConfig || {}; const { videoCodec, supportedCodecs } = serverConfig || {};
@@ -60,7 +63,9 @@ export const CodecSelector: FC<CodecSelectorProps> = () => {
value: pendingSaveCodec, value: pendingSaveCodec,
path: 'videoSettings', path: 'videoSettings',
}); });
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Video codec updated.')); setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.videoCodecUpdated)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
if (serverStatusData.online) { if (serverStatusData.online) {
@@ -4,6 +4,7 @@ import React, { FC, useContext, useState } from 'react';
import { Typography, Table, Modal, Button, Alert } from 'antd'; import { Typography, Table, Modal, Button, Alert } from 'antd';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useTranslation } from 'next-export-i18n';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import { AlertMessageContext } from '../../utils/alert-message-context'; import { AlertMessageContext } from '../../utils/alert-message-context';
import { UpdateArgs, VideoVariant } from '../../types/config-section'; import { UpdateArgs, VideoVariant } from '../../types/config-section';
@@ -24,6 +25,7 @@ import {
STATUS_PROCESSING, STATUS_PROCESSING,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import { Localization } from '../../types/localization';
import { FormStatusIndicator } from './FormStatusIndicator'; import { FormStatusIndicator } from './FormStatusIndicator';
const { Title } = Typography; const { Title } = Typography;
@@ -36,6 +38,7 @@ const DeleteOutlined = dynamic(() => import('@ant-design/icons/DeleteOutlined'),
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export const CurrentVariantsTable: FC = () => { export const CurrentVariantsTable: FC = () => {
const { t } = useTranslation();
const [displayModal, setDisplayModal] = useState(false); const [displayModal, setDisplayModal] = useState(false);
const [modalProcessing, setModalProcessing] = useState(false); const [modalProcessing, setModalProcessing] = useState(false);
const [editId, setEditId] = useState(0); const [editId, setEditId] = useState(0);
@@ -87,7 +90,9 @@ export const CurrentVariantsTable: FC = () => {
setModalProcessing(false); setModalProcessing(false);
handleModalCancel(); handleModalCancel();
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Variants updated')); setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.variantsUpdated)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
if (serverStatusData.online) { if (serverStatusData.online) {
+25 -5
View File
@@ -2,6 +2,7 @@ import { Button, Upload } from 'antd';
import { RcFile } from 'antd/lib/upload/interface'; import { RcFile } from 'antd/lib/upload/interface';
import React, { useState, useContext, FC } from 'react'; import React, { useState, useContext, FC } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useTranslation } from 'next-export-i18n';
import { FormStatusIndicator } from './FormStatusIndicator'; import { FormStatusIndicator } from './FormStatusIndicator';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import { import {
@@ -17,6 +18,7 @@ import {
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import { NEXT_PUBLIC_API_HOST } from '../../utils/apis'; import { NEXT_PUBLIC_API_HOST } from '../../utils/apis';
import { Localization } from '../../types/localization';
import { import {
ACCEPTED_IMAGE_TYPES, ACCEPTED_IMAGE_TYPES,
@@ -37,6 +39,7 @@ const UploadOutlined = dynamic(() => import('@ant-design/icons/UploadOutlined'),
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export const EditLogo: FC = () => { export const EditLogo: FC = () => {
const { t } = useTranslation();
const [logoUrl, setlogoUrl] = useState(null); const [logoUrl, setlogoUrl] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [logoCachedbuster, setLogoCacheBuster] = useState(0); const [logoCachedbuster, setLogoCacheBuster] = useState(0);
@@ -64,16 +67,28 @@ export const EditLogo: FC = () => {
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
return new Promise<void>((res, rej) => { return new Promise<void>((res, rej) => {
if (file.size > MAX_IMAGE_FILESIZE) { if (file.size > MAX_IMAGE_FILESIZE) {
const msg = `File size is too big: ${readableBytes(file.size)}`; const msg = t(Localization.Admin.StatusMessages.fileSizeTooBig, {
setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${msg}`)); size: readableBytes(file.size),
});
setSubmitStatus(
createInputStatus(
STATUS_ERROR,
t(Localization.Admin.StatusMessages.thereWasAnError, { message: msg }),
),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
setLoading(false); setLoading(false);
// eslint-disable-next-line no-promise-executor-return // eslint-disable-next-line no-promise-executor-return
return rej(); return rej();
} }
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) { if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
const msg = `File type is not supported: ${file.type}`; const msg = t(Localization.Admin.StatusMessages.fileTypeNotSupported, { type: file.type });
setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${msg}`)); setSubmitStatus(
createInputStatus(
STATUS_ERROR,
t(Localization.Admin.StatusMessages.thereWasAnError, { message: msg }),
),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
setLoading(false); setLoading(false);
// eslint-disable-next-line no-promise-executor-return // eslint-disable-next-line no-promise-executor-return
@@ -101,7 +116,12 @@ export const EditLogo: FC = () => {
setLogoCacheBuster(Math.floor(Math.random() * 100)); // Force logo to re-load setLogoCacheBuster(Math.floor(Math.random() * 100)); // Force logo to re-load
}, },
onError: (msg: string) => { onError: (msg: string) => {
setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${msg}`)); setSubmitStatus(
createInputStatus(
STATUS_ERROR,
t(Localization.Admin.StatusMessages.thereWasAnError, { message: msg }),
),
);
setLoading(false); setLoading(false);
}, },
}); });
+9 -1
View File
@@ -1,5 +1,6 @@
import { Popconfirm, Button, Typography } from 'antd'; import { Popconfirm, Button, Typography } from 'antd';
import { FC, useContext, useState } from 'react'; import { FC, useContext, useState } from 'react';
import { useTranslation } from 'next-export-i18n';
import { AlertMessageContext } from '../../utils/alert-message-context'; import { AlertMessageContext } from '../../utils/alert-message-context';
import { API_YP_RESET, fetchData } from '../../utils/apis'; import { API_YP_RESET, fetchData } from '../../utils/apis';
@@ -10,10 +11,12 @@ import {
STATUS_PROCESSING, STATUS_PROCESSING,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import { Localization } from '../../types/localization';
import { FormStatusIndicator } from './FormStatusIndicator'; import { FormStatusIndicator } from './FormStatusIndicator';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export const ResetYP: FC = () => { export const ResetYP: FC = () => {
const { t } = useTranslation();
const { setMessage } = useContext(AlertMessageContext); const { setMessage } = useContext(AlertMessageContext);
const [submitStatus, setSubmitStatus] = useState(null); const [submitStatus, setSubmitStatus] = useState(null);
@@ -32,7 +35,12 @@ export const ResetYP: FC = () => {
setSubmitStatus(createInputStatus(STATUS_SUCCESS)); setSubmitStatus(createInputStatus(STATUS_SUCCESS));
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
} catch (error) { } catch (error) {
setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${error}`)); setSubmitStatus(
createInputStatus(
STATUS_ERROR,
t(Localization.Admin.StatusMessages.thereWasAnError, { message: `${error}` }),
),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
} }
}; };
+9 -1
View File
@@ -4,6 +4,7 @@
import React, { useState, useContext, FC } from 'react'; import React, { useState, useContext, FC } from 'react';
import { Switch } from 'antd'; import { Switch } from 'antd';
import { useTranslation } from 'next-export-i18n';
import { import {
createInputStatus, createInputStatus,
StatusState, StatusState,
@@ -11,6 +12,7 @@ import {
STATUS_PROCESSING, STATUS_PROCESSING,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import { Localization } from '../../types/localization';
import { FormStatusIndicator } from './FormStatusIndicator'; import { FormStatusIndicator } from './FormStatusIndicator';
import { RESET_TIMEOUT, postConfigUpdateToAPI } from '../../utils/config-constants'; import { RESET_TIMEOUT, postConfigUpdateToAPI } from '../../utils/config-constants';
@@ -42,6 +44,7 @@ export const ToggleSwitch: FC<ToggleSwitchProps> = ({
useSubmit, useSubmit,
onChange, onChange,
}) => { }) => {
const { t } = useTranslation();
const [submitStatus, setSubmitStatus] = useState<StatusState>(null); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
let resetTimer = null; let resetTimer = null;
@@ -68,7 +71,12 @@ export const ToggleSwitch: FC<ToggleSwitchProps> = ({
setSubmitStatus(createInputStatus(STATUS_SUCCESS)); setSubmitStatus(createInputStatus(STATUS_SUCCESS));
}, },
onError: (message: string) => { onError: (message: string) => {
setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${message}`)); setSubmitStatus(
createInputStatus(
STATUS_ERROR,
t(Localization.Admin.StatusMessages.thereWasAnError, { message }),
),
);
}, },
}); });
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
+9 -1
View File
@@ -1,5 +1,6 @@
import React, { useContext, useState, useEffect, FC } from 'react'; import React, { useContext, useState, useEffect, FC } from 'react';
import { Typography, Slider } from 'antd'; import { Typography, Slider } from 'antd';
import { useTranslation } from 'next-export-i18n';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import { AlertMessageContext } from '../../utils/alert-message-context'; import { AlertMessageContext } from '../../utils/alert-message-context';
import { import {
@@ -14,6 +15,7 @@ import {
STATUS_PROCESSING, STATUS_PROCESSING,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import { Localization } from '../../types/localization';
import { FormStatusIndicator } from './FormStatusIndicator'; import { FormStatusIndicator } from './FormStatusIndicator';
const { Title } = Typography; const { Title } = Typography;
@@ -36,6 +38,7 @@ const SLIDER_COMMENTS = {
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export const VideoLatency: FC = () => { export const VideoLatency: FC = () => {
const { t } = useTranslation();
const [submitStatus, setSubmitStatus] = useState<StatusState>(null); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [selectedOption, setSelectedOption] = useState(null); const [selectedOption, setSelectedOption] = useState(null);
@@ -73,7 +76,12 @@ export const VideoLatency: FC = () => {
value: postValue, value: postValue,
path: 'videoSettings', path: 'videoSettings',
}); });
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Latency buffer level updated.')); setSubmitStatus(
createInputStatus(
STATUS_SUCCESS,
t(Localization.Admin.StatusMessages.latencyBufferUpdated),
),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
if (serverStatusData.online) { if (serverStatusData.online) {
@@ -3,6 +3,7 @@ import React, { FC, useContext, useCallback, useEffect, useState } from 'react';
import { Button, Col, Collapse, Row, Slider, Space } from 'antd'; import { Button, Col, Collapse, Row, Slider, Space } from 'antd';
import Paragraph from 'antd/lib/typography/Paragraph'; import Paragraph from 'antd/lib/typography/Paragraph';
import Title from 'antd/lib/typography/Title'; import Title from 'antd/lib/typography/Title';
import { useTranslation } from 'next-export-i18n';
import { EditCustomStyles } from '../../EditCustomStyles'; import { EditCustomStyles } from '../../EditCustomStyles';
import s from './appearance.module.scss'; import s from './appearance.module.scss';
import { postConfigUpdateToAPI, RESET_TIMEOUT } from '../../../../utils/config-constants'; import { postConfigUpdateToAPI, RESET_TIMEOUT } from '../../../../utils/config-constants';
@@ -12,6 +13,7 @@ import {
STATUS_ERROR, STATUS_ERROR,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../../../utils/input-statuses'; } from '../../../../utils/input-statuses';
import { Localization } from '../../../../types/localization';
import { ServerStatusContext } from '../../../../utils/server-status-context'; import { ServerStatusContext } from '../../../../utils/server-status-context';
import { FormStatusIndicator } from '../../FormStatusIndicator'; import { FormStatusIndicator } from '../../FormStatusIndicator';
@@ -122,6 +124,7 @@ const ColorCollection: FC<ColorCollectionProps> = ({ variables, updateColor }) =
// eslint-disable-next-line react/function-component-definition // eslint-disable-next-line react/function-component-definition
export default function Appearance() { export default function Appearance() {
const { t } = useTranslation();
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData; const { serverConfig, setFieldInConfigState } = serverStatusData;
const { instanceDetails } = serverConfig; const { instanceDetails } = serverConfig;
@@ -179,7 +182,9 @@ export default function Appearance() {
apiPath: ENDPOINT, apiPath: ENDPOINT,
data: { value: {} }, data: { value: {} },
onSuccess: () => { onSuccess: () => {
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.')); setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.updated)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
setCustomValues({}); setCustomValues({});
}, },
@@ -200,7 +205,9 @@ export default function Appearance() {
apiPath: ENDPOINT, apiPath: ENDPOINT,
data: { value: c }, data: { value: c },
onSuccess: () => { onSuccess: () => {
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.')); setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.updated)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
setFieldInConfigState({ setFieldInConfigState({
@@ -1,6 +1,7 @@
/* eslint-disable react/no-array-index-key */ /* eslint-disable react/no-array-index-key */
import React, { useContext, useState, useEffect } from 'react'; import React, { useContext, useState, useEffect } from 'react';
import { Typography, Tag } from 'antd'; import { Typography, Tag } from 'antd';
import { useTranslation } from 'next-export-i18n';
import { ServerStatusContext } from '../../../../utils/server-status-context'; import { ServerStatusContext } from '../../../../utils/server-status-context';
import { import {
FIELD_PROPS_TAGS, FIELD_PROPS_TAGS,
@@ -17,12 +18,14 @@ import {
STATUS_SUCCESS, STATUS_SUCCESS,
STATUS_WARNING, STATUS_WARNING,
} from '../../../../utils/input-statuses'; } from '../../../../utils/input-statuses';
import { Localization } from '../../../../types/localization';
import { TAG_COLOR } from '../../EditValueArray'; import { TAG_COLOR } from '../../EditValueArray';
const { Title } = Typography; const { Title } = Typography;
// eslint-disable-next-line react/function-component-definition // eslint-disable-next-line react/function-component-definition
export default function EditInstanceTags() { export default function EditInstanceTags() {
const { t } = useTranslation();
const [newTagInput, setNewTagInput] = useState<string>(''); const [newTagInput, setNewTagInput] = useState<string>('');
const [submitStatus, setSubmitStatus] = useState<StatusState>(null); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
@@ -58,7 +61,9 @@ export default function EditInstanceTags() {
data: { value: postValue }, data: { value: postValue },
onSuccess: () => { onSuccess: () => {
setFieldInConfigState({ fieldName: 'tags', value: postValue, path: configPath }); setFieldInConfigState({ fieldName: 'tags', value: postValue, path: configPath });
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Tags updated.')); setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.tagsUpdated)),
);
setNewTagInput(''); setNewTagInput('');
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}, },
@@ -81,11 +86,15 @@ export default function EditInstanceTags() {
resetStates(); resetStates();
const newTag = newTagInput.trim(); const newTag = newTagInput.trim();
if (newTag === '') { if (newTag === '') {
setSubmitStatus(createInputStatus(STATUS_WARNING, 'Please enter a tag')); setSubmitStatus(
createInputStatus(STATUS_WARNING, t(Localization.Admin.StatusMessages.pleaseEnterTag)),
);
return; return;
} }
if (tags.some(tag => tag.toLowerCase() === newTag.toLowerCase())) { if (tags.some(tag => tag.toLowerCase() === newTag.toLowerCase())) {
setSubmitStatus(createInputStatus(STATUS_WARNING, 'This tag is already used!')); setSubmitStatus(
createInputStatus(STATUS_WARNING, t(Localization.Admin.StatusMessages.tagAlreadyUsed)),
);
return; return;
} }
@@ -1,6 +1,7 @@
import { Button, Collapse } from 'antd'; import { Button, Collapse } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useContext, useState, useEffect } from 'react'; import React, { useContext, useState, useEffect } from 'react';
import { useTranslation } from 'next-export-i18n';
import { UpdateArgs } from '../../../../types/config-section'; import { UpdateArgs } from '../../../../types/config-section';
import { ServerStatusContext } from '../../../../utils/server-status-context'; import { ServerStatusContext } from '../../../../utils/server-status-context';
import { AlertMessageContext } from '../../../../utils/alert-message-context'; import { AlertMessageContext } from '../../../../utils/alert-message-context';
@@ -18,6 +19,7 @@ import {
STATUS_PROCESSING, STATUS_PROCESSING,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../../../utils/input-statuses'; } from '../../../../utils/input-statuses';
import { Localization } from '../../../../types/localization';
import { TextField } from '../../TextField'; import { TextField } from '../../TextField';
import { FormStatusIndicator } from '../../FormStatusIndicator'; import { FormStatusIndicator } from '../../FormStatusIndicator';
import { isValidUrl } from '../../../../utils/validators'; import { isValidUrl } from '../../../../utils/validators';
@@ -56,6 +58,7 @@ function checkSaveable(formValues: any, currentValues: any) {
// eslint-disable-next-line react/function-component-definition // eslint-disable-next-line react/function-component-definition
export default function EditStorage() { export default function EditStorage() {
const { t } = useTranslation();
const [formDataValues, setFormDataValues] = useState(null); const [formDataValues, setFormDataValues] = useState(null);
const [submitStatus, setSubmitStatus] = useState<StatusState>(null); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
@@ -125,7 +128,9 @@ export default function EditStorage() {
data: { value: postValue }, data: { value: postValue },
onSuccess: () => { onSuccess: () => {
setFieldInConfigState({ fieldName: 's3', value: postValue, path: '' }); setFieldInConfigState({ fieldName: 's3', value: postValue, path: '' });
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.')); setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.updated)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
setAlertMessage( setAlertMessage(
'Changing your storage configuration will take place the next time you start a new stream.', 'Changing your storage configuration will take place the next time you start a new stream.',
@@ -1,5 +1,6 @@
import { Button, Typography } from 'antd'; import { Button, Typography } from 'antd';
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import { useTranslation } from 'next-export-i18n';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
import { TextField, TEXTFIELD_TYPE_TEXTAREA } from '../TextField'; import { TextField, TEXTFIELD_TYPE_TEXTAREA } from '../TextField';
import { import {
@@ -14,12 +15,14 @@ import {
STATUS_ERROR, STATUS_ERROR,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../../utils/input-statuses'; } from '../../../utils/input-statuses';
import { Localization } from '../../../types/localization';
import { UpdateArgs } from '../../../types/config-section'; import { UpdateArgs } from '../../../types/config-section';
import { FormStatusIndicator } from '../FormStatusIndicator'; import { FormStatusIndicator } from '../FormStatusIndicator';
const { Title } = Typography; const { Title } = Typography;
export const BrowserNotify = () => { export const BrowserNotify = () => {
const { t } = useTranslation();
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {}; const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { notifications } = serverConfig || {}; const { notifications } = serverConfig || {};
@@ -77,7 +80,9 @@ export const BrowserNotify = () => {
value: postValue, value: postValue,
path: 'notifications', path: 'notifications',
}); });
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.')); setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.updated)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}, },
onError: (message: string) => { onError: (message: string) => {
@@ -1,5 +1,6 @@
import { Button, Typography } from 'antd'; import { Button, Typography } from 'antd';
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import { useTranslation } from 'next-export-i18n';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
import { TextField } from '../TextField'; import { TextField } from '../TextField';
import { FormStatusIndicator } from '../FormStatusIndicator'; import { FormStatusIndicator } from '../FormStatusIndicator';
@@ -15,11 +16,13 @@ import {
STATUS_ERROR, STATUS_ERROR,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../../utils/input-statuses'; } from '../../../utils/input-statuses';
import { Localization } from '../../../types/localization';
import { UpdateArgs } from '../../../types/config-section'; import { UpdateArgs } from '../../../types/config-section';
const { Title } = Typography; const { Title } = Typography;
export const DiscordNotify = () => { export const DiscordNotify = () => {
const { t } = useTranslation();
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {}; const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { notifications } = serverConfig || {}; const { notifications } = serverConfig || {};
@@ -77,7 +80,9 @@ export const DiscordNotify = () => {
value: postValue, value: postValue,
path: 'notifications', path: 'notifications',
}); });
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.')); setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.updated)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}, },
onError: (message: string) => { onError: (message: string) => {
+52 -1
View File
@@ -83,7 +83,58 @@
"emojiPageDescription": "Here you can upload new custom emojis for usage in the chat. When uploading a new emoji, the filename without extension will be used as emoji name. Additionally, emoji names are case-insensitive. For best results, ensure all emoji have unique names.", "emojiPageDescription": "Here you can upload new custom emojis for usage in the chat. When uploading a new emoji, the filename without extension will be used as emoji name. Additionally, emoji names are case-insensitive. For best results, ensure all emoji have unique names.",
"emojiUploadBulkGuide": "Want to upload custom emojis in bulk? Check out our <a href=\"https://owncast.online/docs/chat/emoji\" rel=\"noopener noreferrer\" target=\"_blank\">Emoji guide</a>.", "emojiUploadBulkGuide": "Want to upload custom emojis in bulk? Check out our <a href=\"https://owncast.online/docs/chat/emoji\" rel=\"noopener noreferrer\" target=\"_blank\">Emoji guide</a>.",
"emojis": "Emojis", "emojis": "Emojis",
"uploadNewEmoji": "Upload new emoji" "uploadNewEmoji": "Upload new emoji",
"StatusMessages": {
"updated": "Updated.",
"tagsUpdated": "Tags updated.",
"variantsUpdated": "Variants updated",
"videoCodecUpdated": "Video codec updated.",
"latencyBufferUpdated": "Latency buffer level updated.",
"deletingEmoji": "Deleting emoji...",
"emojiDeleted": "Emoji deleted",
"convertingEmoji": "Converting emoji...",
"uploadingEmoji": "Uploading emoji...",
"emojiUploadedSuccessfully": "Emoji uploaded successfully!",
"thereWasAnError": "There was an error: {{message}}",
"fileSizeTooBig": "File size is too big: {{size}}",
"fileTypeNotSupported": "File type is not supported: {{type}}",
"pleaseEnterTag": "Please enter a tag",
"tagAlreadyUsed": "This tag is already used!"
},
"Actions": {
"title": "External Actions",
"description": "External action URLs are 3rd party UI you can display, embedded, into your Owncast page when a user clicks on a button to launch your action.",
"readMoreLink": "Read more about how to use actions, with examples, at",
"createNewAction": "Create New Action",
"createNewActionTitle": "Create New Action",
"editActionTitle": "Edit Action",
"modalDescription": "Add the URL for the external action you want to present.",
"onlyHttpsSupported": "Only HTTPS URLs and embeds are supported.",
"readMoreAboutActions": "Read more about external actions.",
"selectActionType": "Select an action type",
"linkOrEmbedUrl": "Link or embed an URL",
"customHtml": "Custom HTML",
"htmlEmbedPlaceholder": "HTML embed code (required)",
"urlPlaceholder": "https://myserver.com/action (required)",
"titlePlaceholder": "Your action title (required)",
"descriptionPlaceholder": "Optional description",
"iconPlaceholder": "https://myserver.com/action/icon.png (optional)",
"optionalBackgroundColor": "Optional background color of the action button.",
"openExternally": "Open in a new tab instead of within your page."
},
"Webhooks": {
"createNewWebhook": "Create New Webhook",
"webhookUrlPlaceholder": "https://myserver.com/webhook",
"selectEvents": "Select the events that will be sent to this webhook.",
"selectAll": "Select all"
},
"AccessTokens": {
"createNewAccessToken": "Create New Access token",
"nameDescription": "The name will be displayed as the chat user when sending messages with this access token.",
"namePlaceholder": "Name of bot, service, or integration",
"selectPermissions": "Select the permissions this access token will have. It cannot be edited after it's created.",
"selectAll": "Select all"
}
}, },
"Common": { "Common": {
"poweredByOwncastVersion": "Powered by <a href='https://owncast.online'>Owncast v{{versionNumber}}</a>" "poweredByOwncastVersion": "Powered by <a href='https://owncast.online'>Owncast v{{versionNumber}}</a>"
+249 -76
View File
File diff suppressed because it is too large Load Diff
+9 -6
View File
@@ -16,12 +16,15 @@ import {
import { format } from 'date-fns'; import { format } from 'date-fns';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useTranslation } from 'next-export-i18n';
import { import {
fetchData, fetchData,
ACCESS_TOKENS, ACCESS_TOKENS,
DELETE_ACCESS_TOKEN, DELETE_ACCESS_TOKEN,
CREATE_ACCESS_TOKEN, CREATE_ACCESS_TOKEN,
} from '../../utils/apis'; } from '../../utils/apis';
import { Localization } from '../../types/localization';
import { Translation } from '../../components/ui/Translation/Translation';
import { AdminLayout } from '../../components/layouts/AdminLayout'; import { AdminLayout } from '../../components/layouts/AdminLayout';
@@ -72,6 +75,7 @@ interface Props {
} }
const NewTokenModal = (props: Props) => { const NewTokenModal = (props: Props) => {
const { onOk, onCancel, open } = props; const { onOk, onCancel, open } = props;
const { t } = useTranslation();
const [selectedScopes, setSelectedScopes] = useState([]); const [selectedScopes, setSelectedScopes] = useState([]);
const [name, setName] = useState(''); const [name, setName] = useState('');
@@ -107,7 +111,7 @@ const NewTokenModal = (props: Props) => {
return ( return (
<Modal <Modal
title="Create New Access token" title={t(Localization.Admin.AccessTokens.createNewAccessToken)}
open={open} open={open}
onOk={saveToken} onOk={saveToken}
onCancel={onCancel} onCancel={onCancel}
@@ -115,18 +119,17 @@ const NewTokenModal = (props: Props) => {
> >
<p> <p>
<p> <p>
The name will be displayed as the chat user when sending messages with this access token. <Translation translationKey={Localization.Admin.AccessTokens.nameDescription} />
</p> </p>
<Input <Input
value={name} value={name}
placeholder="Name of bot, service, or integration" placeholder={t(Localization.Admin.AccessTokens.namePlaceholder)}
onChange={input => setName(input.currentTarget.value)} onChange={input => setName(input.currentTarget.value)}
/> />
</p> </p>
<p> <p>
Select the permissions this access token will have. It cannot be edited after it&apos;s <Translation translationKey={Localization.Admin.AccessTokens.selectPermissions} />
created.
</p> </p>
<Checkbox.Group style={{ width: '100%' }} value={selectedScopes} onChange={onChange}> <Checkbox.Group style={{ width: '100%' }} value={selectedScopes} onChange={onChange}>
<Row>{checkboxes}</Row> <Row>{checkboxes}</Row>
@@ -134,7 +137,7 @@ const NewTokenModal = (props: Props) => {
<p> <p>
<Button type="primary" onClick={selectAll}> <Button type="primary" onClick={selectAll}>
Select all <Translation translationKey={Localization.Admin.AccessTokens.selectAll} />
</Button> </Button>
</p> </p>
</Modal> </Modal>
+34 -20
View File
@@ -4,6 +4,7 @@ import { bbedit } from '@uiw/codemirror-theme-bbedit';
import { html as codeMirrorHTML } from '@codemirror/lang-html'; import { html as codeMirrorHTML } from '@codemirror/lang-html';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import React, { ReactElement, useContext, useEffect, useState } from 'react'; import React, { ReactElement, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'next-export-i18n';
import { FormStatusIndicator } from '../../components/admin/FormStatusIndicator'; import { FormStatusIndicator } from '../../components/admin/FormStatusIndicator';
import { ExternalAction } from '../../interfaces/external-action'; import { ExternalAction } from '../../interfaces/external-action';
import { import {
@@ -14,6 +15,8 @@ import {
import { createInputStatus, STATUS_ERROR, STATUS_SUCCESS } from '../../utils/input-statuses'; import { createInputStatus, STATUS_ERROR, STATUS_SUCCESS } from '../../utils/input-statuses';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import { isValidUrl, DEFAULT_TEXTFIELD_URL_PATTERN } from '../../utils/validators'; import { isValidUrl, DEFAULT_TEXTFIELD_URL_PATTERN } from '../../utils/validators';
import { Localization } from '../../types/localization';
import { Translation } from '../../components/ui/Translation/Translation';
import { AdminLayout } from '../../components/layouts/AdminLayout'; import { AdminLayout } from '../../components/layouts/AdminLayout';
@@ -54,6 +57,7 @@ type ActionType = 'url' | 'html';
const ActionModal = (props: Props) => { const ActionModal = (props: Props) => {
const { onOk, onCancel, open, action } = props; const { onOk, onCancel, open, action } = props;
const { t } = useTranslation();
const [actionType, setActionType] = useState<ActionType>('url'); const [actionType, setActionType] = useState<ActionType>('url');
@@ -120,32 +124,38 @@ const ActionModal = (props: Props) => {
return ( return (
<Modal <Modal
destroyOnClose destroyOnClose
title={action == null ? 'Create New Action' : 'Edit Action'} title={
action == null
? t(Localization.Admin.Actions.createNewActionTitle)
: t(Localization.Admin.Actions.editActionTitle)
}
open={open} open={open}
onOk={save} onOk={save}
onCancel={onCancel} onCancel={onCancel}
okButtonProps={okButtonProps} okButtonProps={okButtonProps}
> >
<Form initialValues={action}> <Form initialValues={action}>
Add the URL for the external action you want to present.{' '} <Translation translationKey={Localization.Admin.Actions.modalDescription} />{' '}
<strong>Only HTTPS URLs and embeds are supported.</strong> <strong>
<Translation translationKey={Localization.Admin.Actions.onlyHttpsSupported} />
</strong>
<p> <p>
<a <a
href="https://owncast.online/thirdparty/actions/" href="https://owncast.online/thirdparty/actions/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
Read more about external actions. <Translation translationKey={Localization.Admin.Actions.readMoreAboutActions} />
</a> </a>
</p> </p>
<Form.Item> <Form.Item>
<Select <Select
value={actionType} value={actionType}
onChange={setActionType} onChange={setActionType}
placeholder="Select an action type" placeholder={t(Localization.Admin.Actions.selectActionType)}
options={[ options={[
{ label: 'Link or embed an URL', value: 'url' }, { label: t(Localization.Admin.Actions.linkOrEmbedUrl), value: 'url' },
{ label: 'Custom HTML', value: 'html' }, { label: t(Localization.Admin.Actions.customHtml), value: 'html' },
]} ]}
/> />
</Form.Item> </Form.Item>
@@ -153,7 +163,7 @@ const ActionModal = (props: Props) => {
<Form.Item name="html"> <Form.Item name="html">
<CodeMirror <CodeMirror
value={actionHTML} value={actionHTML}
placeholder="HTML embed code (required)" placeholder={t(Localization.Admin.Actions.htmlEmbedPlaceholder)}
theme={bbedit} theme={bbedit}
height="200px" height="200px"
extensions={[codeMirrorHTML()]} extensions={[codeMirrorHTML()]}
@@ -164,7 +174,7 @@ const ActionModal = (props: Props) => {
<Form.Item name="url"> <Form.Item name="url">
<Input <Input
required required
placeholder="https://myserver.com/action (required)" placeholder={t(Localization.Admin.Actions.urlPlaceholder)}
onChange={input => setActionUrl(input.currentTarget.value.trim())} onChange={input => setActionUrl(input.currentTarget.value.trim())}
type="url" type="url"
pattern={DEFAULT_TEXTFIELD_URL_PATTERN} pattern={DEFAULT_TEXTFIELD_URL_PATTERN}
@@ -175,21 +185,21 @@ const ActionModal = (props: Props) => {
<Input <Input
value={actionTitle} value={actionTitle}
required required
placeholder="Your action title (required)" placeholder={t(Localization.Admin.Actions.titlePlaceholder)}
onChange={input => setActionTitle(input.currentTarget.value)} onChange={input => setActionTitle(input.currentTarget.value)}
/> />
</Form.Item> </Form.Item>
<Form.Item name="description"> <Form.Item name="description">
<Input <Input
value={actionDescription} value={actionDescription}
placeholder="Optional description" placeholder={t(Localization.Admin.Actions.descriptionPlaceholder)}
onChange={input => setActionDescription(input.currentTarget.value)} onChange={input => setActionDescription(input.currentTarget.value)}
/> />
</Form.Item> </Form.Item>
<Form.Item name="icon"> <Form.Item name="icon">
<Input <Input
value={actionIcon} value={actionIcon}
placeholder="https://myserver.com/action/icon.png (optional)" placeholder={t(Localization.Admin.Actions.iconPlaceholder)}
onChange={input => setActionIcon(input.currentTarget.value)} onChange={input => setActionIcon(input.currentTarget.value)}
/> />
</Form.Item> </Form.Item>
@@ -201,7 +211,7 @@ const ActionModal = (props: Props) => {
onChange={input => setActionColor(input.currentTarget.value)} onChange={input => setActionColor(input.currentTarget.value)}
/> />
</Form.Item> </Form.Item>
Optional background color of the action button. <Translation translationKey={Localization.Admin.Actions.optionalBackgroundColor} />
</div> </div>
{actionType === 'html' ? null : ( {actionType === 'html' ? null : (
<Form.Item name="openExternally"> <Form.Item name="openExternally">
@@ -210,7 +220,7 @@ const ActionModal = (props: Props) => {
defaultChecked={openExternally} defaultChecked={openExternally}
onChange={onOpenExternallyChanged} onChange={onOpenExternallyChanged}
> >
Open in a new tab instead of within your page. <Translation translationKey={Localization.Admin.Actions.openExternally} />
</Checkbox> </Checkbox>
</Form.Item> </Form.Item>
)} )}
@@ -220,6 +230,7 @@ const ActionModal = (props: Props) => {
}; };
const Actions = () => { const Actions = () => {
const { t } = useTranslation();
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {}; const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { externalActions } = serverConfig; const { externalActions } = serverConfig;
@@ -245,7 +256,9 @@ const Actions = () => {
data: { value: actionsData }, data: { value: actionsData },
onSuccess: () => { onSuccess: () => {
setFieldInConfigState({ fieldName: 'externalActions', value: actionsData, path: '' }); setFieldInConfigState({ fieldName: 'externalActions', value: actionsData, path: '' });
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.')); setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.updated)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}, },
onError: (message: string) => { onError: (message: string) => {
@@ -401,13 +414,14 @@ const Actions = () => {
return ( return (
<div> <div>
<Title>External Actions</Title> <Title>
<Translation translationKey={Localization.Admin.Actions.title} />
</Title>
<Paragraph> <Paragraph>
External action URLs are 3rd party UI you can display, embedded, into your Owncast page when <Translation translationKey={Localization.Admin.Actions.description} />
a user clicks on a button to launch your action.
</Paragraph> </Paragraph>
<Paragraph> <Paragraph>
Read more about how to use actions, with examples, at{' '} <Translation translationKey={Localization.Admin.Actions.readMoreLink} />{' '}
<a <a
href="https://owncast.online/thirdparty/?source=admin" href="https://owncast.online/thirdparty/?source=admin"
target="_blank" target="_blank"
@@ -426,7 +440,7 @@ const Actions = () => {
/> />
<br /> <br />
<Button type="primary" onClick={showCreateModal}> <Button type="primary" onClick={showCreateModal}>
Create New Action <Translation translationKey={Localization.Admin.Actions.createNewAction} />
</Button> </Button>
<FormStatusIndicator status={submitStatus} /> <FormStatusIndicator status={submitStatus} />
+18 -5
View File
@@ -67,7 +67,9 @@ const Emoji = () => {
setLoading(true); setLoading(true);
setSubmitStatus(createInputStatus(STATUS_PROCESSING, 'Deleting emoji...')); setSubmitStatus(
createInputStatus(STATUS_PROCESSING, t(Localization.Admin.StatusMessages.deletingEmoji)),
);
try { try {
const response = await fetchData(DELETE_EMOJI, { const response = await fetchData(DELETE_EMOJI, {
@@ -79,7 +81,9 @@ const Emoji = () => {
throw response; throw response;
} }
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Emoji deleted')); setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.emojiDeleted)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
} catch (error) { } catch (error) {
setSubmitStatus(createInputStatus(STATUS_ERROR, `${error}`)); setSubmitStatus(createInputStatus(STATUS_ERROR, `${error}`));
@@ -93,7 +97,9 @@ const Emoji = () => {
async function handleEmojiUpload() { async function handleEmojiUpload() {
setLoading(true); setLoading(true);
try { try {
setSubmitStatus(createInputStatus(STATUS_PROCESSING, 'Converting emoji...')); setSubmitStatus(
createInputStatus(STATUS_PROCESSING, t(Localization.Admin.StatusMessages.convertingEmoji)),
);
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
const emojiData = await new Promise<CustomEmoji>((res, rej) => { const emojiData = await new Promise<CustomEmoji>((res, rej) => {
@@ -111,7 +117,9 @@ const Emoji = () => {
); );
}); });
setSubmitStatus(createInputStatus(STATUS_PROCESSING, 'Uploading emoji...')); setSubmitStatus(
createInputStatus(STATUS_PROCESSING, t(Localization.Admin.StatusMessages.uploadingEmoji)),
);
const response = await fetchData(UPLOAD_EMOJI, { const response = await fetchData(UPLOAD_EMOJI, {
method: 'POST', method: 'POST',
@@ -125,7 +133,12 @@ const Emoji = () => {
throw response; throw response;
} }
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Emoji uploaded successfully!')); setSubmitStatus(
createInputStatus(
STATUS_SUCCESS,
t(Localization.Admin.StatusMessages.emojiUploadedSuccessfully),
),
);
getEmojis(); getEmojis();
} catch (error) { } catch (error) {
setSubmitStatus(createInputStatus(STATUS_ERROR, `${error}`)); setSubmitStatus(createInputStatus(STATUS_ERROR, `${error}`));
+10 -4
View File
@@ -14,8 +14,11 @@ import {
} from 'antd'; } from 'antd';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import React, { ReactElement, useEffect, useState } from 'react'; import React, { ReactElement, useEffect, useState } from 'react';
import { useTranslation } from 'next-export-i18n';
import { CREATE_WEBHOOK, DELETE_WEBHOOK, fetchData, WEBHOOKS } from '../../utils/apis'; import { CREATE_WEBHOOK, DELETE_WEBHOOK, fetchData, WEBHOOKS } from '../../utils/apis';
import { isValidUrl, DEFAULT_TEXTFIELD_URL_PATTERN } from '../../utils/validators'; import { isValidUrl, DEFAULT_TEXTFIELD_URL_PATTERN } from '../../utils/validators';
import { Localization } from '../../types/localization';
import { Translation } from '../../components/ui/Translation/Translation';
import { AdminLayout } from '../../components/layouts/AdminLayout'; import { AdminLayout } from '../../components/layouts/AdminLayout';
@@ -71,6 +74,7 @@ interface Props {
const NewWebhookModal = (props: Props) => { const NewWebhookModal = (props: Props) => {
const { onOk, onCancel, open } = props; const { onOk, onCancel, open } = props;
const { t } = useTranslation();
const [selectedEvents, setSelectedEvents] = useState([]); const [selectedEvents, setSelectedEvents] = useState([]);
const [webhookUrl, setWebhookUrl] = useState(''); const [webhookUrl, setWebhookUrl] = useState('');
@@ -108,7 +112,7 @@ const NewWebhookModal = (props: Props) => {
return ( return (
<Modal <Modal
title="Create New Webhook" title={t(Localization.Admin.Webhooks.createNewWebhook)}
open={open} open={open}
onOk={save} onOk={save}
onCancel={onCancel} onCancel={onCancel}
@@ -117,20 +121,22 @@ const NewWebhookModal = (props: Props) => {
<div> <div>
<Input <Input
value={webhookUrl} value={webhookUrl}
placeholder="https://myserver.com/webhook" placeholder={t(Localization.Admin.Webhooks.webhookUrlPlaceholder)}
onChange={input => setWebhookUrl(input.currentTarget.value.trim())} onChange={input => setWebhookUrl(input.currentTarget.value.trim())}
type="url" type="url"
pattern={DEFAULT_TEXTFIELD_URL_PATTERN} pattern={DEFAULT_TEXTFIELD_URL_PATTERN}
/> />
</div> </div>
<p>Select the events that will be sent to this webhook.</p> <p>
<Translation translationKey={Localization.Admin.Webhooks.selectEvents} />
</p>
<Checkbox.Group style={{ width: '100%' }} value={selectedEvents} onChange={onChange}> <Checkbox.Group style={{ width: '100%' }} value={selectedEvents} onChange={onChange}>
<Row>{checkboxes}</Row> <Row>{checkboxes}</Row>
</Checkbox.Group> </Checkbox.Group>
<p> <p>
<Button type="primary" onClick={selectAll}> <Button type="primary" onClick={selectAll}>
Select all <Translation translationKey={Localization.Admin.Webhooks.selectAll} />
</Button> </Button>
</p> </p>
</Modal> </Modal>
+60
View File
@@ -250,6 +250,66 @@ export const Localization = {
timestamp: 'Admin.timestamp', timestamp: 'Admin.timestamp',
message: 'Admin.message', message: 'Admin.message',
logs: 'Admin.logs', logs: 'Admin.logs',
// Form status messages
StatusMessages: {
updated: 'Admin.StatusMessages.updated',
tagsUpdated: 'Admin.StatusMessages.tagsUpdated',
variantsUpdated: 'Admin.StatusMessages.variantsUpdated',
videoCodecUpdated: 'Admin.StatusMessages.videoCodecUpdated',
latencyBufferUpdated: 'Admin.StatusMessages.latencyBufferUpdated',
deletingEmoji: 'Admin.StatusMessages.deletingEmoji',
emojiDeleted: 'Admin.StatusMessages.emojiDeleted',
convertingEmoji: 'Admin.StatusMessages.convertingEmoji',
uploadingEmoji: 'Admin.StatusMessages.uploadingEmoji',
emojiUploadedSuccessfully: 'Admin.StatusMessages.emojiUploadedSuccessfully',
thereWasAnError: 'Admin.StatusMessages.thereWasAnError',
fileSizeTooBig: 'Admin.StatusMessages.fileSizeTooBig',
fileTypeNotSupported: 'Admin.StatusMessages.fileTypeNotSupported',
pleaseEnterTag: 'Admin.StatusMessages.pleaseEnterTag',
tagAlreadyUsed: 'Admin.StatusMessages.tagAlreadyUsed',
},
// Actions page
Actions: {
title: 'Admin.Actions.title',
description: 'Admin.Actions.description',
readMoreLink: 'Admin.Actions.readMoreLink',
createNewAction: 'Admin.Actions.createNewAction',
createNewActionTitle: 'Admin.Actions.createNewActionTitle',
editActionTitle: 'Admin.Actions.editActionTitle',
modalDescription: 'Admin.Actions.modalDescription',
onlyHttpsSupported: 'Admin.Actions.onlyHttpsSupported',
readMoreAboutActions: 'Admin.Actions.readMoreAboutActions',
selectActionType: 'Admin.Actions.selectActionType',
linkOrEmbedUrl: 'Admin.Actions.linkOrEmbedUrl',
customHtml: 'Admin.Actions.customHtml',
htmlEmbedPlaceholder: 'Admin.Actions.htmlEmbedPlaceholder',
urlPlaceholder: 'Admin.Actions.urlPlaceholder',
titlePlaceholder: 'Admin.Actions.titlePlaceholder',
descriptionPlaceholder: 'Admin.Actions.descriptionPlaceholder',
iconPlaceholder: 'Admin.Actions.iconPlaceholder',
optionalBackgroundColor: 'Admin.Actions.optionalBackgroundColor',
openExternally: 'Admin.Actions.openExternally',
},
// Webhooks page
Webhooks: {
createNewWebhook: 'Admin.Webhooks.createNewWebhook',
webhookUrlPlaceholder: 'Admin.Webhooks.webhookUrlPlaceholder',
selectEvents: 'Admin.Webhooks.selectEvents',
selectAll: 'Admin.Webhooks.selectAll',
},
// Access Tokens page
AccessTokens: {
createNewAccessToken: 'Admin.AccessTokens.createNewAccessToken',
nameDescription: 'Admin.AccessTokens.nameDescription',
namePlaceholder: 'Admin.AccessTokens.namePlaceholder',
selectPermissions: 'Admin.AccessTokens.selectPermissions',
cannotEditAfterCreated: 'Admin.AccessTokens.cannotEditAfterCreated',
selectAll: 'Admin.AccessTokens.selectAll',
},
}, },
/** /**