1
0
Fork 0
mirror of https://github.com/owncast/owncast.git synced 2024-10-28 10:09:39 +01:00

feat: add translations support to admin pages and components (#3977)

* feat: add translations support to admin pages and components

Added translations support admin main page and its components, help
page, handware-info page. Added translations support for LogTable,
NewsFeed and StreamHealthOverview components.

* update package.json

* fix rendering issue

* Commit updated API documentation

---------

Co-authored-by: Owncast <owncast@owncast.online>
Co-authored-by: Gabe Kangas <gabek@real-ity.com>
This commit is contained in:
Sufyaan Khateeb 2024-10-25 00:58:20 +05:30 committed by GitHub
parent 34c3cfcd9a
commit d03d6e79aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 86 additions and 69 deletions

View file

@ -3,6 +3,7 @@ import { Table, Tag, Typography } from 'antd';
import Linkify from 'react-linkify';
import { SortOrder, TablePaginationConfig } from 'antd/lib/table/interface';
import { format } from 'date-fns';
import { useTranslation } from 'next-export-i18n';
const { Title } = Typography;
@ -28,39 +29,41 @@ export type LogTableProps = {
};
export const LogTable: FC<LogTableProps> = ({ logs, initialPageSize }) => {
if (!logs?.length) {
return null;
}
const { t } = useTranslation();
const [pageSize, setPageSize] = useState(initialPageSize);
const handleTableChange = (pagination: TablePaginationConfig) => {
setPageSize(pagination.pageSize);
};
if (!logs?.length) {
return null;
}
const columns = [
{
title: 'Level',
title: t('Level'),
dataIndex: 'level',
key: 'level',
filters: [
{
text: 'Info',
text: t('Info'),
value: 'info',
},
{
text: 'Warning',
text: t('Warning'),
value: 'warning',
},
{
text: 'Error',
value: 'error',
text: t('Error'),
value: 'Error',
},
],
onFilter: (level, row) => row.level.indexOf(level) === 0,
render: renderColumnLevel,
},
{
title: 'Timestamp',
title: t('Timestamp'),
dataIndex: 'time',
key: 'time',
render: timestamp => {
@ -72,7 +75,7 @@ export const LogTable: FC<LogTableProps> = ({ logs, initialPageSize }) => {
defaultSortOrder: 'descend' as SortOrder,
},
{
title: 'Message',
title: t('Message'),
dataIndex: 'message',
key: 'message',
render: renderMessage,
@ -81,7 +84,7 @@ export const LogTable: FC<LogTableProps> = ({ logs, initialPageSize }) => {
return (
<div className="logs-section">
<Title>Logs</Title>
<Title>{t('Logs')}</Title>
<Table
size="middle"
dataSource={logs}

View file

@ -4,6 +4,7 @@ import React, { useState, useEffect, FC } from 'react';
import { Collapse, Typography, Skeleton } from 'antd';
import { format } from 'date-fns';
import { useTranslation } from 'next-export-i18n';
import { fetchExternalData } from '../../utils/apis';
const { Panel } = Collapse;
@ -27,6 +28,7 @@ const ArticleItem: FC<ArticleProps> = ({
date_published: date,
defaultOpen = false,
}) => {
const { t } = useTranslation();
const dateObject = new Date(date);
const dateString = format(dateObject, 'MMM dd, yyyy, HH:mm');
return (
@ -36,7 +38,7 @@ const ArticleItem: FC<ArticleProps> = ({
<p className="timestamp">
{dateString} (
<Link href={`${OWNCAST_BASE_URL}${url}`} target="_blank" rel="noopener noreferrer">
Link
{t('Link')}
</Link>
)
</p>
@ -48,6 +50,7 @@ const ArticleItem: FC<ArticleProps> = ({
};
export const NewsFeed = () => {
const { t } = useTranslation();
const [feed, setFeed] = useState<ArticleProps[]>([]);
const [loading, setLoading] = useState<Boolean>(true);
@ -69,11 +72,11 @@ export const NewsFeed = () => {
}, []);
const loadingSpinner = loading ? <Skeleton loading active /> : null;
const noNews = !loading && feed.length === 0 ? <div>No news.</div> : null;
const noNews = !loading && feed.length === 0 ? <div>{t('No news.')}</div> : null;
return (
<section className="news-feed form-module">
<Title level={2}>News &amp; Updates from Owncast</Title>
<Title level={2}>{t('News & Updates from Owncast')}</Title>
{loadingSpinner}
{feed.map(item => (
<ArticleItem {...item} key={item.url} defaultOpen={feed.length === 1} />

View file

@ -2,6 +2,7 @@ import { Alert, Button, Card, Col, Row, Statistic, Typography } from 'antd';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import React, { FC, useContext } from 'react';
import { useTranslation } from 'next-export-i18n';
import { ServerStatusContext } from '../../utils/server-status-context';
// Lazy loaded components
@ -22,6 +23,7 @@ export type StreamHealthOverviewProps = {
};
export const StreamHealthOverview: FC<StreamHealthOverviewProps> = ({ showTroubleshootButton }) => {
const { t } = useTranslation();
const serverStatusData = useContext(ServerStatusContext);
const { health } = serverStatusData;
if (!health) {
@ -45,15 +47,15 @@ export const StreamHealthOverview: FC<StreamHealthOverviewProps> = ({ showTroubl
<Row gutter={8}>
<Col span={12}>
<Statistic
title="Healthy Stream"
value={healthy ? 'Yes' : 'No'}
title={t('Healthy Stream')}
value={healthy ? t('Yes') : t('No')}
valueStyle={{ color }}
prefix={healthy ? <CheckCircleOutlined /> : <ExclamationCircleOutlined />}
/>
</Col>
<Col span={12}>
<Statistic
title="Playback Health"
title={t('Playback Health')}
value={healthPercentage}
valueStyle={{ color }}
suffix="%"
@ -65,8 +67,7 @@ export const StreamHealthOverview: FC<StreamHealthOverviewProps> = ({ showTroubl
type="secondary"
style={{ textAlign: 'center', fontSize: '0.7em', opacity: '0.3' }}
>
Stream health represents {representation}% of all known players. Other player status is
unknown.
{`${t('Stream health represents')} ${representation}% ${t('of all known players. Other player status is unknown.')}`}
</Typography.Text>
</Row>
<Row
@ -82,7 +83,7 @@ export const StreamHealthOverview: FC<StreamHealthOverviewProps> = ({ showTroubl
showTroubleshootButton && (
<Link passHref href="/admin/stream-health">
<Button size="small" type="text" style={{ color: 'black' }}>
TROUBLESHOOT
{t('TROUBLESHOOT')}
</Button>
</Link>
)

View file

@ -40,10 +40,12 @@
"classnames": "2.5.1",
"date-fns": "^3.0.0",
"graphemer": "^1.4.0",
"i18next-parser": "^8.9.0",
"interweave": "^13.0.0",
"interweave-autolink": "^5.1.0",
"lodash": "4.17.21",
"next": "14.2.15",
"next-export-i18n": "^2.1.0",
"next-pwa": "^5.6.0",
"next-with-less": "3.0.1",
"picmo": "5.8.5",

View file

@ -1,6 +1,7 @@
import { Row, Col, Typography, Alert, Spin } from 'antd';
import React, { ReactElement, useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import { useTranslation } from 'next-export-i18n';
import { fetchData, FETCH_INTERVAL, HARDWARE_STATS } from '../../utils/apis';
import { Chart } from '../../components/admin/Chart';
import { StatisticItem } from '../../components/admin/StatisticItem';
@ -22,6 +23,7 @@ const SaveOutlined = dynamic(() => import('@ant-design/icons/SaveOutlined'), {
});
export default function HardwareInfo() {
const { t } = useTranslation();
const [hardwareStatus, setHardwareStatus] = useState({
cpu: [], // Array<TimedValue>(),
memory: [], // Array<TimedValue>(),
@ -53,13 +55,13 @@ export default function HardwareInfo() {
if (!hardwareStatus.cpu) {
return (
<div>
<Typography.Title>Hardware Info</Typography.Title>
<Typography.Title>{t('Hardware Info')}</Typography.Title>
<Alert
style={{ marginTop: '10px' }}
banner
message="Please wait"
description="No hardware details have been collected yet."
message={t('Please wait')}
description={t('No hardware details have been collected yet.')}
type="info"
/>
<Spin spinning style={{ width: '100%', margin: '10px' }} />
@ -73,19 +75,19 @@ export default function HardwareInfo() {
const series = [
{
name: 'CPU',
name: t('CPU'),
color: '#B63FFF',
data: hardwareStatus.cpu,
pointStyle: 'rect',
},
{
name: 'Memory',
name: t('Memory'),
color: '#2087E2',
data: hardwareStatus.memory,
pointStyle: 'circle',
},
{
name: 'Disk',
name: t('Disk'),
color: '#FF7700',
data: hardwareStatus.disk,
pointStyle: 'rectRounded',
@ -94,7 +96,7 @@ export default function HardwareInfo() {
return (
<>
<Typography.Title>Hardware Info</Typography.Title>
<Typography.Title>{t('Hardware Info')}</Typography.Title>
<br />
<div>
<Row gutter={[16, 16]} justify="space-around">
@ -130,7 +132,7 @@ export default function HardwareInfo() {
</Col>
</Row>
<Chart title="% used" dataCollections={series} color="#FF7700" unit="%" />
<Chart title={`% ${t('used')}`} dataCollections={series} color="#FF7700" unit="%" />
</div>
</>
);

View file

@ -5,6 +5,7 @@ import Title from 'antd/lib/typography/Title';
import React, { ReactElement } from 'react';
import dynamic from 'next/dynamic';
import { useTranslation } from 'next-export-i18n';
import { AdminLayout } from '../../components/layouts/AdminLayout';
// Lazy loaded components
@ -50,10 +51,12 @@ const SlidersTwoTone = dynamic(() => import('@ant-design/icons/SlidersTwoTone'),
});
export default function Help() {
const { t } = useTranslation();
const questions = [
{
icon: <SettingTwoTone style={{ fontSize: '24px' }} />,
title: 'I want to configure my owncast instance',
title: t('I want to configure my owncast instance'),
content: (
<div>
<a
@ -61,14 +64,14 @@ export default function Help() {
target="_blank"
rel="noopener noreferrer"
>
<LinkOutlined /> Learn more
<LinkOutlined /> {t('Learn more')}
</a>
</div>
),
},
{
icon: <CameraTwoTone style={{ fontSize: '24px' }} />,
title: 'Help configuring my broadcasting software',
title: t('Help configuring my broadcasting software'),
content: (
<div>
<a
@ -76,14 +79,14 @@ export default function Help() {
target="_blank"
rel="noopener noreferrer"
>
<LinkOutlined /> Learn more
<LinkOutlined /> {t('Learn more')}
</a>
</div>
),
},
{
icon: <Html5TwoTone style={{ fontSize: '24px' }} />,
title: 'I want to embed my stream into another site',
title: t('I want to embed my stream into another site'),
content: (
<div>
<a
@ -91,14 +94,14 @@ export default function Help() {
target="_blank"
rel="noopener noreferrer"
>
<LinkOutlined /> Learn more
<LinkOutlined /> {t('Learn more')}
</a>
</div>
),
},
{
icon: <EditTwoTone style={{ fontSize: '24px' }} />,
title: 'I want to customize my website',
title: t('I want to customize my website'),
content: (
<div>
<a
@ -106,14 +109,14 @@ export default function Help() {
target="_blank"
rel="noopener noreferrer"
>
<LinkOutlined /> Learn more
<LinkOutlined /> {t('Learn more')}
</a>
</div>
),
},
{
icon: <SlidersTwoTone style={{ fontSize: '24px' }} />,
title: 'I want to tweak my video output',
title: t('I want to tweak my video output'),
content: (
<div>
<a
@ -121,14 +124,14 @@ export default function Help() {
target="_blank"
rel="noopener noreferrer"
>
<LinkOutlined /> Learn more
<LinkOutlined /> {t('Learn more')}
</a>
</div>
),
},
{
icon: <DatabaseTwoTone style={{ fontSize: '24px' }} />,
title: 'I want to use an external storage provider',
title: t('I want to use an external storage provider'),
content: (
<div>
<a
@ -136,7 +139,7 @@ export default function Help() {
target="_blank"
rel="noopener noreferrer"
>
<LinkOutlined /> Learn more
<LinkOutlined /> {t('Learn more')}
</a>
</div>
),
@ -146,58 +149,58 @@ export default function Help() {
const otherResources = [
{
icon: <BugTwoTone style={{ fontSize: '24px' }} />,
title: 'I found a bug',
title: t('I found a bug'),
content: (
<div>
If you found a bug, then please
{t('If you found a bug, then please')}
<a
href="https://github.com/owncast/owncast/issues/new/choose"
target="_blank"
rel="noopener noreferrer"
>
{' '}
let us know
{t('let us know')}
</a>
</div>
),
},
{
icon: <QuestionCircleTwoTone style={{ fontSize: '24px' }} />,
title: 'I have a general question',
title: t('I have a general question'),
content: (
<div>
Most general questions are answered in our
{t('Most general questions are answered in our')}
<a
href="https://owncast.online/faq/?source=admin"
target="_blank"
rel="noopener noreferrer"
>
{' '}
FAQ
{t('FAQ')}
</a>{' '}
or exist in our{' '}
{t('or exist in our')}{' '}
<a
href="https://github.com/owncast/owncast/discussions"
target="_blank"
rel="noopener noreferrer"
>
discussions
{t('discussions')}
</a>
</div>
),
},
{
icon: <ApiTwoTone style={{ fontSize: '24px' }} />,
title: 'I want to build add-ons for Owncast',
title: t('I want to build add-ons for Owncast'),
content: (
<div>
You can build your own bots, overlays, tools and add-ons with our
{t('You can build your own bots, overlays, tools and add-ons with our')}
<a
href="https://owncast.online/thirdparty?source=admin"
target="_blank"
rel="noopener noreferrer"
>
&nbsp;developer APIs.&nbsp;
&nbsp;{t('developer APIs.')}&nbsp;
</a>
</div>
),
@ -206,11 +209,11 @@ export default function Help() {
return (
<div className="help-page">
<Title style={{ textAlign: 'center' }}>How can we help you?</Title>
<Title style={{ textAlign: 'center' }}>{t('How can we help you?')}</Title>
<Row gutter={[16, 16]} justify="space-around" align="middle">
<Col xs={24} lg={12} style={{ textAlign: 'center' }}>
<Result status="500" />
<Title level={2}>Troubleshooting</Title>
<Title level={2}>{t('Troubleshooting')}</Title>
<Button
target="_blank"
rel="noopener noreferrer"
@ -218,12 +221,12 @@ export default function Help() {
icon={<LinkOutlined />}
type="primary"
>
Fix your problems
{t('Fix your problems')}
</Button>
</Col>
<Col xs={24} lg={12} style={{ textAlign: 'center' }}>
<Result status="404" />
<Title level={2}>Documentation</Title>
<Title level={2}>{t('Documentation')}</Title>
<Button
target="_blank"
rel="noopener noreferrer"
@ -231,12 +234,12 @@ export default function Help() {
icon={<LinkOutlined />}
type="primary"
>
Read the Docs
{t('Read the Docs')}
</Button>
</Col>
</Row>
<Divider />
<Title level={2}>Common tasks</Title>
<Title level={2}>{t('Common tasks')}</Title>
<Row gutter={[16, 16]}>
{questions.map(question => (
<Col xs={24} lg={12} key={question.title}>
@ -247,7 +250,7 @@ export default function Help() {
))}
</Row>
<Divider />
<Title level={2}>Other</Title>
<Title level={2}>{t('Other')}</Title>
<Row gutter={[16, 16]}>
{otherResources.map(question => (
<Col xs={24} lg={12} key={question.title}>

View file

@ -3,6 +3,7 @@ import React, { useState, useEffect, useContext, ReactElement } from 'react';
import { Skeleton, Card, Statistic, Row, Col } from 'antd';
import { formatDistanceToNow, formatRelative } from 'date-fns';
import dynamic from 'next/dynamic';
import { useTranslation } from 'next-export-i18n';
import { ServerStatusContext } from '../../utils/server-status-context';
import { LogTable } from '../../components/admin/LogTable';
import { Offline } from '../../components/admin/Offline';
@ -39,6 +40,8 @@ function streamDetailsFormatter(streamDetails) {
}
export default function Home() {
const { t } = useTranslation();
const serverStatusData = useContext(ServerStatusContext);
const { broadcaster, serverConfig: configData } = serverStatusData || {};
const { remoteAddr, streamDetails } = broadcaster || {};
@ -101,12 +104,12 @@ export default function Home() {
<div className="stream-details-item-container">
<Statistic
className="stream-details-item"
title="Outbound Video Stream"
title={t('Outbound Video Stream')}
value={videoSetting}
/>
<Statistic
className="stream-details-item"
title="Outbound Audio Stream"
title={t('Outbound Audio Stream')}
value={audioSetting}
/>
</div>
@ -130,17 +133,17 @@ export default function Home() {
<Row gutter={[16, 16]} align="middle">
<Col span={8} sm={24} md={8}>
<Statistic
title={`Stream started ${formatRelative(broadcastDate, Date.now())}`}
title={`${t('Stream started')} ${formatRelative(broadcastDate, Date.now())}`}
value={formatDistanceToNow(broadcastDate)}
prefix={<ClockCircleOutlined />}
/>
</Col>
<Col span={8} sm={24} md={8}>
<Statistic title="Viewers" value={viewerCount} prefix={<UserOutlined />} />
<Statistic title={t('Viewers')} value={viewerCount} prefix={<UserOutlined />} />
</Col>
<Col span={8} sm={24} md={8}>
<Statistic
title="Peak viewer count"
title={t('Peak viewer count')}
value={sessionPeakViewerCount}
prefix={<UserOutlined />}
/>
@ -154,28 +157,28 @@ export default function Home() {
<Col className="stream-details" span={12} sm={24} md={24} lg={12}>
<Card
size="small"
title="Outbound Stream Details"
title={t('Outbound Stream Details')}
type="inner"
className="outbound-details"
>
{videoQualitySettings}
</Card>
<Card size="small" title="Inbound Stream Details" type="inner">
<Card size="small" title={t('Inbound Stream Details')} type="inner">
<Statistic
className="stream-details-item"
title="Input"
title={t('Input')}
value={`${encoder} ${formatIPAddress(remoteAddr)}`}
/>
<Statistic
className="stream-details-item"
title="Inbound Video Stream"
title={t('Inbound Video Stream')}
value={streamDetails}
formatter={streamDetailsFormatter}
/>
<Statistic
className="stream-details-item"
title="Inbound Audio Stream"
title={t('Inbound Audio Stream')}
value={streamAudioDetailString}
/>
</Card>