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 React, { FC, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'next-export-i18n';
import { AlertMessageContext } from '../../utils/alert-message-context';
import {
API_VIDEO_CODEC,
@@ -13,11 +14,13 @@ import {
STATUS_SUCCESS,
} from '../../utils/input-statuses';
import { ServerStatusContext } from '../../utils/server-status-context';
import { Localization } from '../../types/localization';
import { FormStatusIndicator } from './FormStatusIndicator';
export type CodecSelectorProps = {};
export const CodecSelector: FC<CodecSelectorProps> = () => {
const { t } = useTranslation();
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { videoCodec, supportedCodecs } = serverConfig || {};
@@ -60,7 +63,9 @@ export const CodecSelector: FC<CodecSelectorProps> = () => {
value: pendingSaveCodec,
path: 'videoSettings',
});
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Video codec updated.'));
setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.videoCodecUpdated)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
if (serverStatusData.online) {
@@ -4,6 +4,7 @@ import React, { FC, useContext, useState } from 'react';
import { Typography, Table, Modal, Button, Alert } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import dynamic from 'next/dynamic';
import { useTranslation } from 'next-export-i18n';
import { ServerStatusContext } from '../../utils/server-status-context';
import { AlertMessageContext } from '../../utils/alert-message-context';
import { UpdateArgs, VideoVariant } from '../../types/config-section';
@@ -24,6 +25,7 @@ import {
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../../utils/input-statuses';
import { Localization } from '../../types/localization';
import { FormStatusIndicator } from './FormStatusIndicator';
const { Title } = Typography;
@@ -36,6 +38,7 @@ const DeleteOutlined = dynamic(() => import('@ant-design/icons/DeleteOutlined'),
// eslint-disable-next-line import/prefer-default-export
export const CurrentVariantsTable: FC = () => {
const { t } = useTranslation();
const [displayModal, setDisplayModal] = useState(false);
const [modalProcessing, setModalProcessing] = useState(false);
const [editId, setEditId] = useState(0);
@@ -87,7 +90,9 @@ export const CurrentVariantsTable: FC = () => {
setModalProcessing(false);
handleModalCancel();
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Variants updated'));
setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.variantsUpdated)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
if (serverStatusData.online) {
+25 -5
View File
@@ -2,6 +2,7 @@ import { Button, Upload } from 'antd';
import { RcFile } from 'antd/lib/upload/interface';
import React, { useState, useContext, FC } from 'react';
import dynamic from 'next/dynamic';
import { useTranslation } from 'next-export-i18n';
import { FormStatusIndicator } from './FormStatusIndicator';
import { ServerStatusContext } from '../../utils/server-status-context';
import {
@@ -17,6 +18,7 @@ import {
STATUS_SUCCESS,
} from '../../utils/input-statuses';
import { NEXT_PUBLIC_API_HOST } from '../../utils/apis';
import { Localization } from '../../types/localization';
import {
ACCEPTED_IMAGE_TYPES,
@@ -37,6 +39,7 @@ const UploadOutlined = dynamic(() => import('@ant-design/icons/UploadOutlined'),
// eslint-disable-next-line import/prefer-default-export
export const EditLogo: FC = () => {
const { t } = useTranslation();
const [logoUrl, setlogoUrl] = useState(null);
const [loading, setLoading] = useState(false);
const [logoCachedbuster, setLogoCacheBuster] = useState(0);
@@ -64,16 +67,28 @@ export const EditLogo: FC = () => {
// eslint-disable-next-line consistent-return
return new Promise<void>((res, rej) => {
if (file.size > MAX_IMAGE_FILESIZE) {
const msg = `File size is too big: ${readableBytes(file.size)}`;
setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${msg}`));
const msg = t(Localization.Admin.StatusMessages.fileSizeTooBig, {
size: readableBytes(file.size),
});
setSubmitStatus(
createInputStatus(
STATUS_ERROR,
t(Localization.Admin.StatusMessages.thereWasAnError, { message: msg }),
),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
setLoading(false);
// eslint-disable-next-line no-promise-executor-return
return rej();
}
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
const msg = `File type is not supported: ${file.type}`;
setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${msg}`));
const msg = t(Localization.Admin.StatusMessages.fileTypeNotSupported, { type: file.type });
setSubmitStatus(
createInputStatus(
STATUS_ERROR,
t(Localization.Admin.StatusMessages.thereWasAnError, { message: msg }),
),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
setLoading(false);
// 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
},
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);
},
});
+9 -1
View File
@@ -1,5 +1,6 @@
import { Popconfirm, Button, Typography } from 'antd';
import { FC, useContext, useState } from 'react';
import { useTranslation } from 'next-export-i18n';
import { AlertMessageContext } from '../../utils/alert-message-context';
import { API_YP_RESET, fetchData } from '../../utils/apis';
@@ -10,10 +11,12 @@ import {
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../../utils/input-statuses';
import { Localization } from '../../types/localization';
import { FormStatusIndicator } from './FormStatusIndicator';
// eslint-disable-next-line import/prefer-default-export
export const ResetYP: FC = () => {
const { t } = useTranslation();
const { setMessage } = useContext(AlertMessageContext);
const [submitStatus, setSubmitStatus] = useState(null);
@@ -32,7 +35,12 @@ export const ResetYP: FC = () => {
setSubmitStatus(createInputStatus(STATUS_SUCCESS));
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
} 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);
}
};
+9 -1
View File
@@ -4,6 +4,7 @@
import React, { useState, useContext, FC } from 'react';
import { Switch } from 'antd';
import { useTranslation } from 'next-export-i18n';
import {
createInputStatus,
StatusState,
@@ -11,6 +12,7 @@ import {
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../../utils/input-statuses';
import { Localization } from '../../types/localization';
import { FormStatusIndicator } from './FormStatusIndicator';
import { RESET_TIMEOUT, postConfigUpdateToAPI } from '../../utils/config-constants';
@@ -42,6 +44,7 @@ export const ToggleSwitch: FC<ToggleSwitchProps> = ({
useSubmit,
onChange,
}) => {
const { t } = useTranslation();
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
let resetTimer = null;
@@ -68,7 +71,12 @@ export const ToggleSwitch: FC<ToggleSwitchProps> = ({
setSubmitStatus(createInputStatus(STATUS_SUCCESS));
},
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);
+9 -1
View File
@@ -1,5 +1,6 @@
import React, { useContext, useState, useEffect, FC } from 'react';
import { Typography, Slider } from 'antd';
import { useTranslation } from 'next-export-i18n';
import { ServerStatusContext } from '../../utils/server-status-context';
import { AlertMessageContext } from '../../utils/alert-message-context';
import {
@@ -14,6 +15,7 @@ import {
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../../utils/input-statuses';
import { Localization } from '../../types/localization';
import { FormStatusIndicator } from './FormStatusIndicator';
const { Title } = Typography;
@@ -36,6 +38,7 @@ const SLIDER_COMMENTS = {
// eslint-disable-next-line import/prefer-default-export
export const VideoLatency: FC = () => {
const { t } = useTranslation();
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [selectedOption, setSelectedOption] = useState(null);
@@ -73,7 +76,12 @@ export const VideoLatency: FC = () => {
value: postValue,
path: 'videoSettings',
});
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Latency buffer level updated.'));
setSubmitStatus(
createInputStatus(
STATUS_SUCCESS,
t(Localization.Admin.StatusMessages.latencyBufferUpdated),
),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
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 Paragraph from 'antd/lib/typography/Paragraph';
import Title from 'antd/lib/typography/Title';
import { useTranslation } from 'next-export-i18n';
import { EditCustomStyles } from '../../EditCustomStyles';
import s from './appearance.module.scss';
import { postConfigUpdateToAPI, RESET_TIMEOUT } from '../../../../utils/config-constants';
@@ -12,6 +13,7 @@ import {
STATUS_ERROR,
STATUS_SUCCESS,
} from '../../../../utils/input-statuses';
import { Localization } from '../../../../types/localization';
import { ServerStatusContext } from '../../../../utils/server-status-context';
import { FormStatusIndicator } from '../../FormStatusIndicator';
@@ -122,6 +124,7 @@ const ColorCollection: FC<ColorCollectionProps> = ({ variables, updateColor }) =
// eslint-disable-next-line react/function-component-definition
export default function Appearance() {
const { t } = useTranslation();
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData;
const { instanceDetails } = serverConfig;
@@ -179,7 +182,9 @@ export default function Appearance() {
apiPath: ENDPOINT,
data: { value: {} },
onSuccess: () => {
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.updated)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
setCustomValues({});
},
@@ -200,7 +205,9 @@ export default function Appearance() {
apiPath: ENDPOINT,
data: { value: c },
onSuccess: () => {
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.updated)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
setFieldInConfigState({
@@ -1,6 +1,7 @@
/* eslint-disable react/no-array-index-key */
import React, { useContext, useState, useEffect } from 'react';
import { Typography, Tag } from 'antd';
import { useTranslation } from 'next-export-i18n';
import { ServerStatusContext } from '../../../../utils/server-status-context';
import {
FIELD_PROPS_TAGS,
@@ -17,12 +18,14 @@ import {
STATUS_SUCCESS,
STATUS_WARNING,
} from '../../../../utils/input-statuses';
import { Localization } from '../../../../types/localization';
import { TAG_COLOR } from '../../EditValueArray';
const { Title } = Typography;
// eslint-disable-next-line react/function-component-definition
export default function EditInstanceTags() {
const { t } = useTranslation();
const [newTagInput, setNewTagInput] = useState<string>('');
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
@@ -58,7 +61,9 @@ export default function EditInstanceTags() {
data: { value: postValue },
onSuccess: () => {
setFieldInConfigState({ fieldName: 'tags', value: postValue, path: configPath });
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Tags updated.'));
setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.tagsUpdated)),
);
setNewTagInput('');
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
},
@@ -81,11 +86,15 @@ export default function EditInstanceTags() {
resetStates();
const newTag = newTagInput.trim();
if (newTag === '') {
setSubmitStatus(createInputStatus(STATUS_WARNING, 'Please enter a tag'));
setSubmitStatus(
createInputStatus(STATUS_WARNING, t(Localization.Admin.StatusMessages.pleaseEnterTag)),
);
return;
}
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;
}
@@ -1,6 +1,7 @@
import { Button, Collapse } from 'antd';
import classNames from 'classnames';
import React, { useContext, useState, useEffect } from 'react';
import { useTranslation } from 'next-export-i18n';
import { UpdateArgs } from '../../../../types/config-section';
import { ServerStatusContext } from '../../../../utils/server-status-context';
import { AlertMessageContext } from '../../../../utils/alert-message-context';
@@ -18,6 +19,7 @@ import {
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../../../../utils/input-statuses';
import { Localization } from '../../../../types/localization';
import { TextField } from '../../TextField';
import { FormStatusIndicator } from '../../FormStatusIndicator';
import { isValidUrl } from '../../../../utils/validators';
@@ -56,6 +58,7 @@ function checkSaveable(formValues: any, currentValues: any) {
// eslint-disable-next-line react/function-component-definition
export default function EditStorage() {
const { t } = useTranslation();
const [formDataValues, setFormDataValues] = useState(null);
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
@@ -125,7 +128,9 @@ export default function EditStorage() {
data: { value: postValue },
onSuccess: () => {
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);
setAlertMessage(
'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 React, { useState, useContext, useEffect } from 'react';
import { useTranslation } from 'next-export-i18n';
import { ServerStatusContext } from '../../../utils/server-status-context';
import { TextField, TEXTFIELD_TYPE_TEXTAREA } from '../TextField';
import {
@@ -14,12 +15,14 @@ import {
STATUS_ERROR,
STATUS_SUCCESS,
} from '../../../utils/input-statuses';
import { Localization } from '../../../types/localization';
import { UpdateArgs } from '../../../types/config-section';
import { FormStatusIndicator } from '../FormStatusIndicator';
const { Title } = Typography;
export const BrowserNotify = () => {
const { t } = useTranslation();
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { notifications } = serverConfig || {};
@@ -77,7 +80,9 @@ export const BrowserNotify = () => {
value: postValue,
path: 'notifications',
});
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.updated)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
},
onError: (message: string) => {
@@ -1,5 +1,6 @@
import { Button, Typography } from 'antd';
import React, { useState, useContext, useEffect } from 'react';
import { useTranslation } from 'next-export-i18n';
import { ServerStatusContext } from '../../../utils/server-status-context';
import { TextField } from '../TextField';
import { FormStatusIndicator } from '../FormStatusIndicator';
@@ -15,11 +16,13 @@ import {
STATUS_ERROR,
STATUS_SUCCESS,
} from '../../../utils/input-statuses';
import { Localization } from '../../../types/localization';
import { UpdateArgs } from '../../../types/config-section';
const { Title } = Typography;
export const DiscordNotify = () => {
const { t } = useTranslation();
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { notifications } = serverConfig || {};
@@ -77,7 +80,9 @@ export const DiscordNotify = () => {
value: postValue,
path: 'notifications',
});
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
setSubmitStatus(
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.updated)),
);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
},
onError: (message: string) => {