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:
co-authored by
gabek
Copilot
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Gabe Kangas
parent
fd89c6e8f2
commit
383b80851b
@@ -16,12 +16,15 @@ import {
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useTranslation } from 'next-export-i18n';
|
||||
import {
|
||||
fetchData,
|
||||
ACCESS_TOKENS,
|
||||
DELETE_ACCESS_TOKEN,
|
||||
CREATE_ACCESS_TOKEN,
|
||||
} from '../../utils/apis';
|
||||
import { Localization } from '../../types/localization';
|
||||
import { Translation } from '../../components/ui/Translation/Translation';
|
||||
|
||||
import { AdminLayout } from '../../components/layouts/AdminLayout';
|
||||
|
||||
@@ -72,6 +75,7 @@ interface Props {
|
||||
}
|
||||
const NewTokenModal = (props: Props) => {
|
||||
const { onOk, onCancel, open } = props;
|
||||
const { t } = useTranslation();
|
||||
const [selectedScopes, setSelectedScopes] = useState([]);
|
||||
const [name, setName] = useState('');
|
||||
|
||||
@@ -107,7 +111,7 @@ const NewTokenModal = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Create New Access token"
|
||||
title={t(Localization.Admin.AccessTokens.createNewAccessToken)}
|
||||
open={open}
|
||||
onOk={saveToken}
|
||||
onCancel={onCancel}
|
||||
@@ -115,18 +119,17 @@ const NewTokenModal = (props: Props) => {
|
||||
>
|
||||
<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>
|
||||
<Input
|
||||
value={name}
|
||||
placeholder="Name of bot, service, or integration"
|
||||
placeholder={t(Localization.Admin.AccessTokens.namePlaceholder)}
|
||||
onChange={input => setName(input.currentTarget.value)}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Select the permissions this access token will have. It cannot be edited after it's
|
||||
created.
|
||||
<Translation translationKey={Localization.Admin.AccessTokens.selectPermissions} />
|
||||
</p>
|
||||
<Checkbox.Group style={{ width: '100%' }} value={selectedScopes} onChange={onChange}>
|
||||
<Row>{checkboxes}</Row>
|
||||
@@ -134,7 +137,7 @@ const NewTokenModal = (props: Props) => {
|
||||
|
||||
<p>
|
||||
<Button type="primary" onClick={selectAll}>
|
||||
Select all
|
||||
<Translation translationKey={Localization.Admin.AccessTokens.selectAll} />
|
||||
</Button>
|
||||
</p>
|
||||
</Modal>
|
||||
|
||||
+34
-20
@@ -4,6 +4,7 @@ import { bbedit } from '@uiw/codemirror-theme-bbedit';
|
||||
import { html as codeMirrorHTML } from '@codemirror/lang-html';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React, { ReactElement, useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'next-export-i18n';
|
||||
import { FormStatusIndicator } from '../../components/admin/FormStatusIndicator';
|
||||
import { ExternalAction } from '../../interfaces/external-action';
|
||||
import {
|
||||
@@ -14,6 +15,8 @@ import {
|
||||
import { createInputStatus, STATUS_ERROR, STATUS_SUCCESS } from '../../utils/input-statuses';
|
||||
import { ServerStatusContext } from '../../utils/server-status-context';
|
||||
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';
|
||||
|
||||
@@ -54,6 +57,7 @@ type ActionType = 'url' | 'html';
|
||||
|
||||
const ActionModal = (props: Props) => {
|
||||
const { onOk, onCancel, open, action } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [actionType, setActionType] = useState<ActionType>('url');
|
||||
|
||||
@@ -120,32 +124,38 @@ const ActionModal = (props: Props) => {
|
||||
return (
|
||||
<Modal
|
||||
destroyOnClose
|
||||
title={action == null ? 'Create New Action' : 'Edit Action'}
|
||||
title={
|
||||
action == null
|
||||
? t(Localization.Admin.Actions.createNewActionTitle)
|
||||
: t(Localization.Admin.Actions.editActionTitle)
|
||||
}
|
||||
open={open}
|
||||
onOk={save}
|
||||
onCancel={onCancel}
|
||||
okButtonProps={okButtonProps}
|
||||
>
|
||||
<Form initialValues={action}>
|
||||
Add the URL for the external action you want to present.{' '}
|
||||
<strong>Only HTTPS URLs and embeds are supported.</strong>
|
||||
<Translation translationKey={Localization.Admin.Actions.modalDescription} />{' '}
|
||||
<strong>
|
||||
<Translation translationKey={Localization.Admin.Actions.onlyHttpsSupported} />
|
||||
</strong>
|
||||
<p>
|
||||
<a
|
||||
href="https://owncast.online/thirdparty/actions/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read more about external actions.
|
||||
<Translation translationKey={Localization.Admin.Actions.readMoreAboutActions} />
|
||||
</a>
|
||||
</p>
|
||||
<Form.Item>
|
||||
<Select
|
||||
value={actionType}
|
||||
onChange={setActionType}
|
||||
placeholder="Select an action type"
|
||||
placeholder={t(Localization.Admin.Actions.selectActionType)}
|
||||
options={[
|
||||
{ label: 'Link or embed an URL', value: 'url' },
|
||||
{ label: 'Custom HTML', value: 'html' },
|
||||
{ label: t(Localization.Admin.Actions.linkOrEmbedUrl), value: 'url' },
|
||||
{ label: t(Localization.Admin.Actions.customHtml), value: 'html' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -153,7 +163,7 @@ const ActionModal = (props: Props) => {
|
||||
<Form.Item name="html">
|
||||
<CodeMirror
|
||||
value={actionHTML}
|
||||
placeholder="HTML embed code (required)"
|
||||
placeholder={t(Localization.Admin.Actions.htmlEmbedPlaceholder)}
|
||||
theme={bbedit}
|
||||
height="200px"
|
||||
extensions={[codeMirrorHTML()]}
|
||||
@@ -164,7 +174,7 @@ const ActionModal = (props: Props) => {
|
||||
<Form.Item name="url">
|
||||
<Input
|
||||
required
|
||||
placeholder="https://myserver.com/action (required)"
|
||||
placeholder={t(Localization.Admin.Actions.urlPlaceholder)}
|
||||
onChange={input => setActionUrl(input.currentTarget.value.trim())}
|
||||
type="url"
|
||||
pattern={DEFAULT_TEXTFIELD_URL_PATTERN}
|
||||
@@ -175,21 +185,21 @@ const ActionModal = (props: Props) => {
|
||||
<Input
|
||||
value={actionTitle}
|
||||
required
|
||||
placeholder="Your action title (required)"
|
||||
placeholder={t(Localization.Admin.Actions.titlePlaceholder)}
|
||||
onChange={input => setActionTitle(input.currentTarget.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="description">
|
||||
<Input
|
||||
value={actionDescription}
|
||||
placeholder="Optional description"
|
||||
placeholder={t(Localization.Admin.Actions.descriptionPlaceholder)}
|
||||
onChange={input => setActionDescription(input.currentTarget.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="icon">
|
||||
<Input
|
||||
value={actionIcon}
|
||||
placeholder="https://myserver.com/action/icon.png (optional)"
|
||||
placeholder={t(Localization.Admin.Actions.iconPlaceholder)}
|
||||
onChange={input => setActionIcon(input.currentTarget.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -201,7 +211,7 @@ const ActionModal = (props: Props) => {
|
||||
onChange={input => setActionColor(input.currentTarget.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
Optional background color of the action button.
|
||||
<Translation translationKey={Localization.Admin.Actions.optionalBackgroundColor} />
|
||||
</div>
|
||||
{actionType === 'html' ? null : (
|
||||
<Form.Item name="openExternally">
|
||||
@@ -210,7 +220,7 @@ const ActionModal = (props: Props) => {
|
||||
defaultChecked={openExternally}
|
||||
onChange={onOpenExternallyChanged}
|
||||
>
|
||||
Open in a new tab instead of within your page.
|
||||
<Translation translationKey={Localization.Admin.Actions.openExternally} />
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
)}
|
||||
@@ -220,6 +230,7 @@ const ActionModal = (props: Props) => {
|
||||
};
|
||||
|
||||
const Actions = () => {
|
||||
const { t } = useTranslation();
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
|
||||
const { externalActions } = serverConfig;
|
||||
@@ -245,7 +256,9 @@ const Actions = () => {
|
||||
data: { value: actionsData },
|
||||
onSuccess: () => {
|
||||
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);
|
||||
},
|
||||
onError: (message: string) => {
|
||||
@@ -401,13 +414,14 @@ const Actions = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>External Actions</Title>
|
||||
<Title>
|
||||
<Translation translationKey={Localization.Admin.Actions.title} />
|
||||
</Title>
|
||||
<Paragraph>
|
||||
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.
|
||||
<Translation translationKey={Localization.Admin.Actions.description} />
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Read more about how to use actions, with examples, at{' '}
|
||||
<Translation translationKey={Localization.Admin.Actions.readMoreLink} />{' '}
|
||||
<a
|
||||
href="https://owncast.online/thirdparty/?source=admin"
|
||||
target="_blank"
|
||||
@@ -426,7 +440,7 @@ const Actions = () => {
|
||||
/>
|
||||
<br />
|
||||
<Button type="primary" onClick={showCreateModal}>
|
||||
Create New Action
|
||||
<Translation translationKey={Localization.Admin.Actions.createNewAction} />
|
||||
</Button>
|
||||
<FormStatusIndicator status={submitStatus} />
|
||||
|
||||
|
||||
@@ -67,7 +67,9 @@ const Emoji = () => {
|
||||
|
||||
setLoading(true);
|
||||
|
||||
setSubmitStatus(createInputStatus(STATUS_PROCESSING, 'Deleting emoji...'));
|
||||
setSubmitStatus(
|
||||
createInputStatus(STATUS_PROCESSING, t(Localization.Admin.StatusMessages.deletingEmoji)),
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await fetchData(DELETE_EMOJI, {
|
||||
@@ -79,7 +81,9 @@ const Emoji = () => {
|
||||
throw response;
|
||||
}
|
||||
|
||||
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Emoji deleted'));
|
||||
setSubmitStatus(
|
||||
createInputStatus(STATUS_SUCCESS, t(Localization.Admin.StatusMessages.emojiDeleted)),
|
||||
);
|
||||
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||
} catch (error) {
|
||||
setSubmitStatus(createInputStatus(STATUS_ERROR, `${error}`));
|
||||
@@ -93,7 +97,9 @@ const Emoji = () => {
|
||||
async function handleEmojiUpload() {
|
||||
setLoading(true);
|
||||
try {
|
||||
setSubmitStatus(createInputStatus(STATUS_PROCESSING, 'Converting emoji...'));
|
||||
setSubmitStatus(
|
||||
createInputStatus(STATUS_PROCESSING, t(Localization.Admin.StatusMessages.convertingEmoji)),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
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, {
|
||||
method: 'POST',
|
||||
@@ -125,7 +133,12 @@ const Emoji = () => {
|
||||
throw response;
|
||||
}
|
||||
|
||||
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Emoji uploaded successfully!'));
|
||||
setSubmitStatus(
|
||||
createInputStatus(
|
||||
STATUS_SUCCESS,
|
||||
t(Localization.Admin.StatusMessages.emojiUploadedSuccessfully),
|
||||
),
|
||||
);
|
||||
getEmojis();
|
||||
} catch (error) {
|
||||
setSubmitStatus(createInputStatus(STATUS_ERROR, `${error}`));
|
||||
|
||||
@@ -14,8 +14,11 @@ import {
|
||||
} from 'antd';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'next-export-i18n';
|
||||
import { CREATE_WEBHOOK, DELETE_WEBHOOK, fetchData, WEBHOOKS } from '../../utils/apis';
|
||||
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';
|
||||
|
||||
@@ -71,6 +74,7 @@ interface Props {
|
||||
|
||||
const NewWebhookModal = (props: Props) => {
|
||||
const { onOk, onCancel, open } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [selectedEvents, setSelectedEvents] = useState([]);
|
||||
const [webhookUrl, setWebhookUrl] = useState('');
|
||||
@@ -108,7 +112,7 @@ const NewWebhookModal = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Create New Webhook"
|
||||
title={t(Localization.Admin.Webhooks.createNewWebhook)}
|
||||
open={open}
|
||||
onOk={save}
|
||||
onCancel={onCancel}
|
||||
@@ -117,20 +121,22 @@ const NewWebhookModal = (props: Props) => {
|
||||
<div>
|
||||
<Input
|
||||
value={webhookUrl}
|
||||
placeholder="https://myserver.com/webhook"
|
||||
placeholder={t(Localization.Admin.Webhooks.webhookUrlPlaceholder)}
|
||||
onChange={input => setWebhookUrl(input.currentTarget.value.trim())}
|
||||
type="url"
|
||||
pattern={DEFAULT_TEXTFIELD_URL_PATTERN}
|
||||
/>
|
||||
</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}>
|
||||
<Row>{checkboxes}</Row>
|
||||
</Checkbox.Group>
|
||||
<p>
|
||||
<Button type="primary" onClick={selectAll}>
|
||||
Select all
|
||||
<Translation translationKey={Localization.Admin.Webhooks.selectAll} />
|
||||
</Button>
|
||||
</p>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user