Initial localization work (#3980)

* First pass at configuring localization

* Add CI job for translations

* Update CI job

* Update default value

* Update parser config

* Update defaults again

* try to fix the multiple parsing of a file

* Update crowdlin config

* Update configs

* New Crowdin translations by GitHub Action (#3448)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>

* Point to updated translated files

* Tooltip i18n

* Run translation job when web components are updated

* Commit updated translations

* Translations update (#3453)

* Update source file strings.json
Updated translations

* New translations strings.json (French)
Updated translations

* New translations strings.json (Spanish)
Updated translations

* New translations strings.json (German)
Updated translations

* New translations strings.json (English, United States)
Updated translations

* Commit updated translations

* New Crowdin translations by GitHub Action (#3452)

Co-authored-by: Owncast <owncast@owncast.online>

* chore(deps): update to next config to address build errors

* New Crowdin translations by GitHub Action (#3455)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>

* Translations update (#3456)

* New translations strings.json (Arabic)
Updated translations

* New translations strings.json (German)
Updated translations

* New translations strings.json (Greek)
Updated translations

* New translations strings.json (Irish)
Updated translations

* New translations strings.json (Italian)
Updated translations

* New translations strings.json (Japanese)
Updated translations

* New translations strings.json (Korean)
Updated translations

* New translations strings.json (Dutch)
Updated translations

* New translations strings.json (Norwegian)
Updated translations

* New translations strings.json (Punjabi)
Updated translations

* New translations strings.json (Russian)
Updated translations

* New translations strings.json (Swedish)
Updated translations

* New translations strings.json (Chinese Traditional)
Updated translations

* New translations strings.json (Vietnamese)
Updated translations

* New translations strings.json (Bengali)
Updated translations

* New translations strings.json (Thai)
Updated translations

* New translations strings.json (Croatian)
Updated translations

* New translations strings.json (Hindi)
Updated translations

* New translations strings.json (Malay)
Updated translations

* New Crowdin translations by GitHub Action (#3457)

* New translations strings.json (Arabic)
Updated translations

* New translations strings.json (German)
Updated translations

* New translations strings.json (Greek)
Updated translations

* New translations strings.json (Irish)
Updated translations

* New translations strings.json (Italian)
Updated translations

* New translations strings.json (Japanese)
Updated translations

* New translations strings.json (Korean)
Updated translations

* New translations strings.json (Dutch)
Updated translations

* New translations strings.json (Norwegian)
Updated translations

* New translations strings.json (Punjabi)
Updated translations

* New translations strings.json (Russian)
Updated translations

* New translations strings.json (Swedish)
Updated translations

* New translations strings.json (Chinese Traditional)
Updated translations

* New translations strings.json (Vietnamese)
Updated translations

* New translations strings.json (Bengali)
Updated translations

* New translations strings.json (Thai)
Updated translations

* New translations strings.json (Croatian)
Updated translations

* New translations strings.json (Hindi)
Updated translations

* New translations strings.json (Malay)
Updated translations

* New Crowdin translations by GitHub Action

---------

Co-authored-by: Gabe Kangas <gabek@real-ity.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>

* Commit updated API documentation

* Update translations job

* New Crowdin translations by GitHub Action (#3698)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>

* Update Crowdin configuration file

* Translations update (#3700)

* New translations strings.json (French)
Updated translations

* New translations strings.json (Italian)
Updated translations

* Translations update (#3699)

* New translations strings.json (French)
Updated translations

* New translations strings.json (Spanish)
Updated translations

* New translations strings.json (Italian)
Updated translations

* New translations strings.json (Japanese)
Updated translations

* New translations strings.json (Polish)
Updated translations

* New translations strings.json (Russian)
Updated translations

* New translations strings.json (Portuguese, Brazilian)
Updated translations

* Commit updated API documentation

---------

Co-authored-by: Owncast <owncast@owncast.online>

* New Crowdin translations by GitHub Action (#3701)

* New translations strings.json (French)
Updated translations

* New translations strings.json (Spanish)
Updated translations

* New translations strings.json (Italian)
Updated translations

* New translations strings.json (Japanese)
Updated translations

* New translations strings.json (Polish)
Updated translations

* New translations strings.json (Russian)
Updated translations

* New translations strings.json (Portuguese, Brazilian)
Updated translations

* New Crowdin translations by GitHub Action

---------

Co-authored-by: Gabe Kangas <gabek@real-ity.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>

* Draft: Mark strings for translation. (#3458)

* Mark strings for translation.

* Mark up strings for translation

* fix(web): fix linter warnings

---------

Co-authored-by: Le fractal <17422-fractal@users.noreply.framagit.org>
Co-authored-by: Gabe Kangas <gabek@real-ity.com>

* do not pull from cowdin via workflow

* Commit updated translations

* 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>

* Offline banner i18n formatting (#3997)

* Fix "Last live ago" string formatting with i18n interpolation

* Change some base translation jsons to use i18n interpolation

* Linting fix

* chore(js): ignore i18n pkgs in knip

* fix(test): fix browser ui test

* fix(js): remove unused var

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Owncast <owncast@owncast.online>
Co-authored-by: taintedcypher <119351153+taintedcypher@users.noreply.github.com>
Co-authored-by: Le fractal <17422-fractal@users.noreply.framagit.org>
Co-authored-by: Sufyaan Khateeb <81009832+SufyaanKhateeb@users.noreply.github.com>
Co-authored-by: mahmed2000 <mahmad2000@protonmail.com>
This commit is contained in:
Gabe Kangas
2025-01-14 20:54:21 -08:00
committed by GitHub
parent b45552ade0
commit cb387d88be
51 changed files with 6889 additions and 5026 deletions

55
.github/workflows/translations.yml vendored Normal file
View File

@ -0,0 +1,55 @@
name: Translation job
on:
push:
paths:
- 'web/i18n/strings.json'
- 'web/**/*.tsx'
- 'web/**/*.js'
- 'crowdin.yml'
- '.github/workflows/translations.yml'
- 'web/i18next-parser.config.mjs'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
generate-translations:
defaults:
run:
working-directory: ./web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }}
run: npm install
- name: Generate translation files
run: npm run translate
- name: Crowdin upload sources/download translations
uses: crowdin/github-action@v1
with:
upload_sources: true
download_translations: false
localization_branch_name: translations
config: crowdin.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Commit changes
uses: EndBug/add-and-commit@v9
with:
author_name: Owncast
author_email: owncast@owncast.online
message: 'Commit updated translations'
add: 'web/i18n/**'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

10
crowdin.yml Normal file
View File

@ -0,0 +1,10 @@
project_id_env: CROWDIN_PROJECT_ID
api_token_env: CROWDIN_PERSONAL_TOKEN
pull_request_title: Translations update
pull_request_labels:
- crowdin
- i18n
commit_message: Updated translations
files:
- source: /web/i18n/strings.json
translation: /web/i18n/%two_letters_code%.json

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,10 @@
// TODO: Fire API call to enable federation and set domain, and stream username.
import fetchData from '../../support/fetchData.js';
import { setup } from '../../support/setup.js';
setup();
describe('Fediverse tests', () => {
// Enable Fediverse features.
before(() => {
it('Can visit the page', () => {
fetchData('http://localhost:8080/api/admin/config/serverurl', {
method: 'POST',
data: { value: 'https://testing.biz' },
@ -13,11 +13,10 @@ describe('Fediverse tests', () => {
method: 'POST',
data: { value: true },
});
});
cy.wait(1500);
it('Can visit the page', () => {
cy.visit('http://localhost:8080/');
cy.reload(true);
// cy.reload(true, { timeout: 10000 });
});
// Offline banner

View File

@ -16,10 +16,22 @@
"ignoreDependencies": [
"@fontsource/inter",
"@fontsource/poppins",
"@next/bundle-analyzer",
"autoprefixer",
"yaml",
"sharp",
"workbox-precaching",
"workbox-window",
"@storybook/addon-a11y",
"@storybook/addon-actions",
"@storybook/addon-docs",
"@storybook/addon-essentials",
"@storybook/addon-links",
"@storybook/addon-postcss",
"@storybook/addon-viewport",
"@storybook/cli",
"@storybook/mdx2-csf",
"@storybook/preset-scss",
"@mdx-js/react",
"@storybook/testing-library",
"@svgr/webpack",
@ -34,6 +46,9 @@
"install",
"mdx-mermaid",
"mermaid",
"sass-loader",
"sb",
"storybook-addon-fetch-mock",
"storybook-preset-less",
"ts-jest",
"stylelint-config-standard",
@ -46,6 +61,16 @@
"@commitlint/cli",
"@commitlint/config-conventional",
"babel-plugin-dynamic-import-node",
"@babel/preset-react"
"@babel/preset-react",
"postcss",
"stylelint",
"@babel/core",
"@storybook/addon-mdx-gfm",
"@storybook/addon-styling-webpack",
"@storybook/nextjs",
"@storybook/react",
"resolve-url-loader",
"i18next-parser",
"i18next-scanner"
]
}

View File

@ -1,6 +1,7 @@
import { Button } from 'antd';
import { FC } from 'react';
import dynamic from 'next/dynamic';
import { useTranslation } from 'next-export-i18n';
import styles from './ActionButton/ActionButton.module.scss';
// Lazy loaded components
@ -14,7 +15,10 @@ export type NotifyButtonProps = {
onClick?: () => void;
};
export const NotifyButton: FC<NotifyButtonProps> = ({ onClick, text }) => (
export const NotifyButton: FC<NotifyButtonProps> = ({ onClick, text }) => {
const { t } = useTranslation();
return (
<Button
type="primary"
className={styles.button}
@ -22,6 +26,7 @@ export const NotifyButton: FC<NotifyButtonProps> = ({ onClick, text }) => (
onClick={onClick}
id="notify-button"
>
{text || 'Notify'}
{text || t('Notify')}
</Button>
);
};

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

@ -1,3 +1,4 @@
import { useTranslation } from 'next-export-i18n';
import { Card, Col, Row, Typography } from 'antd';
import Link from 'next/link';
import { FC, useContext } from 'react';
@ -43,6 +44,7 @@ export type OfflineProps = {
export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
const serverStatusData = useContext(ServerStatusContext);
const { t } = useTranslation();
const { serverConfig } = serverStatusData || {};
const { rtmpServerPort, streamKeyOverridden } = serverConfig;
const instanceUrl = global.window?.location.hostname || '';
@ -55,7 +57,7 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
const data = [
{
icon: <BookTwoTone twoToneColor="#6f42c1" />,
title: 'Use your broadcasting software',
title: t('Use your broadcasting software'),
content: (
<div>
<a
@ -63,12 +65,13 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
target="_blank"
rel="noopener noreferrer"
>
Learn how to point your existing software to your new server and start streaming your
content.
{t(
'Learn how to point your existing software to your new server and start streaming your content.',
)}
</a>
<div className="stream-info-container">
<Text strong className="stream-info-label">
Streaming URL:
{t('Streaming URL:')}
</Text>
{rtmpURL && (
<Paragraph className="stream-info-box" copyable>
@ -76,14 +79,14 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
</Paragraph>
)}
<Text strong className="stream-info-label">
Streaming Keys:
{t('Streaming Keys:')}
</Text>
<Text strong className="stream-info-box">
{!streamKeyOverridden ? (
<Link href="/admin/config/server"> View </Link>
<Link href="/admin/config/server"> {t('View')} </Link>
) : (
<span style={{ paddingLeft: '10px', fontWeight: 'normal' }}>
Overridden via command line.
{t('Overridden via command line.')}
</span>
)}
</Text>
@ -93,7 +96,7 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
},
{
icon: <PlaySquareTwoTone twoToneColor="#f9826c" />,
title: 'Embed your video onto other sites',
title: t('Embed your video onto other sites'),
content: (
<div>
<a
@ -101,7 +104,7 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
target="_blank"
rel="noopener noreferrer"
>
Learn how you can add your Owncast stream to other sites you control.
{t('Learn how you can add your Owncast stream to other sites you control.')}
</a>
</div>
),
@ -111,19 +114,19 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
if (!config?.chatDisabled) {
data.push({
icon: <MessageTwoTone twoToneColor="#0366d6" />,
title: 'Chat is disabled',
content: <span>Chat will continue to be disabled until you begin a live stream.</span>,
title: t('Chat is disabled'),
content: <span>{t('Chat will continue to be disabled until you begin a live stream.')}</span>,
});
}
if (!config?.yp?.enabled) {
data.push({
icon: <ProfileTwoTone twoToneColor="#D18BFE" />,
title: 'Find an audience on the Owncast Directory',
title: t('Find an audience on the Owncast Directory'),
content: (
<div>
List yourself in the Owncast Directory and show off your stream. Enable it in{' '}
<Link href="/admin/config/general/">settings.</Link>
{t('List yourself in the Owncast Directory and show off your stream. Enable it in')}{' '}
<Link href="/admin/config/general/">{t('settings.')}</Link>
</div>
),
});
@ -132,12 +135,13 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
if (!config?.federation?.enabled) {
data.push({
icon: <img alt="fediverse" width="20px" src="/img/fediverse-color.png" />,
title: 'Add your Owncast instance to the Fediverse',
title: t('Add your Owncast instance to the Fediverse'),
content: (
<div>
<Link href="/admin/config-federation/">Enable Owncast social</Link> features to have your
instance join the Fediverse, allowing people to follow, share and engage with your live
stream.
<Link href="/admin/config-federation/">{t('Enable Owncast social features')}</Link>{' '}
{t(
'to have your instance join the Fediverse, allowing people to follow, share and engage with your live stream.',
)}
</div>
),
});
@ -152,8 +156,8 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
<OwncastLogo variant="simple" />
</span>
<div>
<Title level={2}>No stream is active</Title>
<p>You should start one.</p>
<Title level={2}>{t('No stream is active')}</Title>
<p>{t('You should start one.')}</p>
</div>
</div>
</Col>

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

@ -1,5 +1,6 @@
import { FC } from 'react';
import { useRecoilValue } from 'recoil';
import { useTranslation } from 'next-export-i18n';
import styles from './Footer.module.scss';
import { ServerStatus } from '../../../interfaces/server-status.model';
import { serverStatusState } from '../../stores/ClientConfigStore';
@ -7,20 +8,21 @@ import { serverStatusState } from '../../stores/ClientConfigStore';
export const Footer: FC = () => {
const clientStatus = useRecoilValue<ServerStatus>(serverStatusState);
const { versionNumber } = clientStatus;
const { t } = useTranslation();
return (
<footer className={styles.footer} id="footer">
<span>
Powered by <a href="https://owncast.online">Owncast v{versionNumber}</a>
{t('Powered by Owncast')} <a href="https://owncast.online">v{versionNumber}</a>
</span>
<span className={styles.links}>
<a href="https://owncast.online/docs" target="_blank" rel="noreferrer">
Documentation
{t('Documentation')}
</a>
<a href="https://owncast.online/help" target="_blank" rel="noreferrer">
Contribute
{t('Contribute')}
</a>
<a href="https://github.com/owncast/owncast" target="_blank" rel="noreferrer">
Source
{t('Source')}
</a>
</span>
</footer>

View File

@ -3,6 +3,7 @@ import { FC, useEffect, useState } from 'react';
import cn from 'classnames';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import { useTranslation } from 'next-export-i18n';
import styles from './Header.module.scss';
// Lazy loaded components
@ -23,6 +24,7 @@ export type HeaderComponentProps = {
export const Header: FC<HeaderComponentProps> = ({ name, chatAvailable, chatDisabled, online }) => {
const [canHideChat, setCanHideChat] = useState(false);
const { t } = useTranslation();
useEffect(() => {
setCanHideChat(window.innerWidth >= 768);
@ -32,18 +34,18 @@ export const Header: FC<HeaderComponentProps> = ({ name, chatAvailable, chatDisa
<header className={cn([`${styles.header}`], 'global-header')}>
{online ? (
<Link href="#player" className={styles.skipLink}>
Skip to player
{t('Skip to player')}
</Link>
) : (
<Link href="#offline-message" className={styles.skipLink}>
Skip to offline message
{t('Skip to offline message')}
</Link>
)}
<Link href="#skip-to-content" className={styles.skipLink}>
Skip to page content
{t('Skip to page content')}
</Link>
<Link href="#footer" className={styles.skipLink}>
Skip to footer
{t('Skip to footer')}
</Link>
<div className={styles.logo}>
<div id="header-logo" className={styles.logoImage}>
@ -59,10 +61,10 @@ export const Header: FC<HeaderComponentProps> = ({ name, chatAvailable, chatDisa
{!chatAvailable && !chatDisabled && (
<Tooltip
overlayClassName={styles.toolTip}
title="Chat will be available when the stream is live."
title={t('Chat will be available when the stream is live.')}
placement="left"
>
<span className={styles.chatOfflineText}>Chat is offline</span>
<span className={styles.chatOfflineText}>{t('Chat is offline')}</span>
</Tooltip>
)}
</header>

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect, FC } from 'react';
import dynamic from 'next/dynamic';
import { useTranslation } from 'next-export-i18n';
import styles from './NotifyReminderPopup.module.scss';
import { Popover } from '../Popover/Popover';
@ -24,6 +25,7 @@ export const NotifyReminderPopup: FC<NotifyReminderPopupProps> = ({
}) => {
const [openPopup, setOpenPopup] = useState(open);
const [mounted, setMounted] = useState(false);
const { t } = useTranslation();
useEffect(() => {
setOpenPopup(open);
@ -33,7 +35,7 @@ export const NotifyReminderPopup: FC<NotifyReminderPopupProps> = ({
setMounted(true);
}, []);
const title = <div className={styles.title}>Stay updated!</div>;
const title = <div className={styles.title}>{t('Stay updated!')}</div>;
const popupClicked = e => {
e.stopPropagation();
@ -56,7 +58,7 @@ export const NotifyReminderPopup: FC<NotifyReminderPopupProps> = ({
>
<CloseOutlined />
</button>
<div className={styles.contentbutton}>Click and never miss future streams!</div>
<div className={styles.contentbutton}>{t('Click and never miss future streams!')}</div>
</div>
);

View File

@ -5,6 +5,7 @@ import { FC } from 'react';
import { formatDistanceToNow } from 'date-fns';
import dynamic from 'next/dynamic';
import classNames from 'classnames';
import { useTranslation } from 'next-export-i18n';
import styles from './OfflineBanner.module.scss';
// Lazy loaded components
@ -36,13 +37,15 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
onFollowClick,
className,
}) => {
const { t } = useTranslation();
let text;
if (customText) {
text = customText;
} else if (!customText && notificationsEnabled && fediverseAccount) {
text = (
<span>
This stream is offline. You can{' '}
{t('This stream is offline. You can')}{' '}
<span role="link" tabIndex={0} className={styles.actionLink} onClick={onNotifyClick}>
be notified
</span>{' '}
@ -56,21 +59,25 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
} else if (!customText && notificationsEnabled) {
text = (
<span>
This stream is offline.{' '}
{t('This stream is offline')}.{' '}
<span role="link" tabIndex={0} className={styles.actionLink} onClick={onNotifyClick}>
Be notified
</span>{' '}
the next time {streamName} goes live.
{t('the next time goes live', { streamer: streamName })}.
</span>
);
} else if (!customText && fediverseAccount) {
text = (
<span>
This stream is offline.{' '}
{t('This stream is offline.')}{' '}
<span role="link" tabIndex={0} className={styles.actionLink} onClick={onFollowClick}>
Follow
{t('Follow')}
</span>{' '}
{fediverseAccount} on the Fediverse to see the next time {streamName} goes live.
{t('on the Fediverse to see the next time goes live', {
fediverseAccount,
streamer: streamName,
})}
.
</span>
);
} else {
@ -95,7 +102,7 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
{lastLive && (
<div className={styles.lastLiveDate}>
<ClockCircleOutlined className={styles.clockIcon} />
{`Last live ${formatDistanceToNow(new Date(lastLive))} ago.`}
{`${t('Last live ago', { timeAgo: formatDistanceToNow(new Date(lastLive)) })}`}
</div>
)}
</div>

20
web/i18n/ar.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/bn.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/de.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Benachrichtigen",
"Powered by Owncast": "Betrieben von Owncast",
"Documentation": "Dokumentation",
"Contribute": "Beitragen",
"Source": "Quelle",
"Skip to player": "Zum Spieler springen",
"Skip to offline message": "Zur Offline-Nachricht springen",
"Skip to page content": "Direkt zum Hauptinhalt",
"Skip to footer": "Zum Footer springen",
"Chat will be available when the stream is live": "Chat ist verfügbar, wenn der Stream live ist.",
"Chat is offline": "Chat ist offline",
"Stay updated!": "Bleiben Sie auf dem Laufenden Ihrer Tätigkeiten!",
"Click and never miss future streams!": "Klicke und verpasse niemals zukünftige Streams!",
"This stream is offline": "Dieser Stream ist offline.",
"the next time goes live": "das nächste Mal geht live",
"Follow": "Folgen",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Zuletzt live"
}

20
web/i18n/el.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/en.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/es.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notificar",
"Powered by Owncast": "Desarrollado por Owncast",
"Documentation": "Documentación",
"Contribute": "Contribuir",
"Source": "Fuente",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/fr.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Avertir",
"Powered by Owncast": "Propulsé par Owncast",
"Documentation": "Documentation",
"Contribute": "Contribuer",
"Source": "Source",
"Skip to player": "Passer au joueur",
"Skip to offline message": "Aller au message d'absence",
"Skip to page content": "Aller au contenu principal",
"Skip to footer": "Aller au pied de page",
"Chat will be available when the stream is live": "Le chat sera disponible quand le direct débutera.",
"Chat is offline": "Le chat est hors ligne",
"Stay updated!": "Restez à jour !",
"Click and never miss future streams!": "Cliquez et ne manquez jamais les futurs diffusions !",
"This stream is offline": "Ce serveur est hors-ligne.",
"the next time goes live": "la prochaine fois que diffuse en direct",
"Follow": "Suivre",
"on the Fediverse to see the next time goes live": "sur le Fédiverse pour voir la prochaine fois que lance un direct",
"Last live ago": "Dernière diffusion il y a"
}

20
web/i18n/ga.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/hi.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/hr.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

19
web/i18n/index.js Normal file
View File

@ -0,0 +1,19 @@
const en = require('./en.json');
const es = require('./es.json');
const de = require('./de.json');
const fr = require('./fr.json');
const i18n = {
translations: {
en,
es,
de,
fr,
},
defaultLang: 'en',
useBrowserDefault: true,
// optional property, will default to "query" if not set
languageDataStore: 'query' || 'localStorage',
};
module.exports = i18n;

20
web/i18n/it.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notifica",
"Powered by Owncast": "Alimentato da Owncast",
"Documentation": "Documentazione",
"Contribute": "Contribuisci",
"Source": "Fonte",
"Skip to player": "Vai al lettore video",
"Skip to offline message": "Vai al messaggio offline",
"Skip to page content": "Vai al contenuto della pagina",
"Skip to footer": "Vai a piè di pagina",
"Chat will be available when the stream is live": "La chat sarà disponibile quando lo stream è in diretta.",
"Chat is offline": "La chat è offline",
"Stay updated!": "Rimani aggiornato!",
"Click and never miss future streams!": "Clicca e non perderti mai gli stream futuri!",
"This stream is offline": "Questo stream è offline.",
"the next time goes live": "la prossima volta che andrà in diretta",
"Follow": "Segui",
"on the Fediverse to see the next time goes live": "sul Fediverso per vedere la prossima volta che andrà in diretta",
"Last live ago": "Ultima diretta fa"
}

20
web/i18n/ja.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "通知",
"Powered by Owncast": "Owncastによる提供",
"Documentation": "ドキュメンテーション",
"Contribute": "貢献",
"Source": "ソース",
"Skip to player": "プレイヤーへスキップ",
"Skip to offline message": "オフラインメッセージへスキップ",
"Skip to page content": "メインコンテンツへスキップ",
"Skip to footer": "フッターへスキップ",
"Chat will be available when the stream is live": "チャットは配信が始まると利用できます",
"Chat is offline": "チャットはオフラインです",
"Stay updated!": "通知を受け取るようにしましょう",
"Click and never miss future streams!": "配信を見逃さないようにクリックしましょう",
"This stream is offline": "配信はオフラインです",
"the next time goes live": "次の配信で",
"Follow": "フォロー",
"on the Fediverse to see the next time goes live": "Fediverseで次回のライブを見るために",
"Last live ago": "前回の配信は 前でした"
}

20
web/i18n/ko.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/ms.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/nl.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/no.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/pa.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/pl.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/pt.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notificar",
"Powered by Owncast": "Desenvolvido por Owncast",
"Documentation": "Documentação",
"Contribute": "Contribua",
"Source": "Fonte",
"Skip to player": "Pular para o player",
"Skip to offline message": "Pular para mensagem offline",
"Skip to page content": "Pular para página de conteúdo",
"Skip to footer": "Pular para o rodapé",
"Chat will be available when the stream is live": "O Chat estará disponível quando a transmissão estiver ativa.",
"Chat is offline": "O chat está off-line",
"Stay updated!": "Mantenha-se atualizado!",
"Click and never miss future streams!": "Clique e não perca futuras transmissões!",
"This stream is offline": "Esta transmissão não está ativa.",
"the next time goes live": "a próxima transmissão será",
"Follow": "Siga",
"on the Fediverse to see the next time goes live": "no Fediverse para ver a data da próxima transmissão",
"Last live ago": "Última transmissão foi"
}

20
web/i18n/ru.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Оповещать",
"Powered by Owncast": "Работает на Owncast",
"Documentation": "Документация",
"Contribute": "Внести вклад",
"Source": "Исходный код",
"Skip to player": "Перейти к плееру",
"Skip to offline message": "Перейти к сообщениям офлайн",
"Skip to page content": "Перейти к основному содержимому",
"Skip to footer": "Перейти к нижнему колонтитулу",
"Chat will be available when the stream is live": "Чат будет доступен, когда будет запущен прямой эфир.",
"Chat is offline": "Чат не в сети",
"Stay updated!": "Будьте в курсе!",
"Click and never miss future streams!": "Кликните и никогда не пропустите будущие стримы!",
"This stream is offline": "Эта трансляция выключена.",
"the next time goes live": "в следующий раз, когда выйдет в эфир",
"Follow": "Подписаться",
"on the Fediverse to see the next time goes live": "на Fedivers, чтобы посмотреть, когда в следующий раз выйдет в прямой эфир",
"Last live ago": "Последний эфир назад"
}

1
web/i18n/strings.json Normal file
View File

@ -0,0 +1 @@
{}

20
web/i18n/strings_old.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/sv.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/th.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/vi.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

20
web/i18n/zh.json Normal file
View File

@ -0,0 +1,20 @@
{
"Notify": "Notify",
"Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation",
"Contribute": "Contribute",
"Source": "Source",
"Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline",
"Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.",
"the next time goes live": "the next time {{streamer}} goes live",
"Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live {{timeAgo}} ago"
}

View File

@ -0,0 +1,113 @@
// i18next-parser.config.js
export default {
contextSeparator: '_',
// Key separator used in your translation keys
createOldCatalogs: true,
// Save the \_old files
defaultNamespace: 'translation',
// Default namespace used in your i18next config
defaultValue: function (locale, namespace, key, value) {
return `${key}`;
}, // Default value to give to keys with no value
// You may also specify a function accepting the locale, namespace, key, and value as arguments
indentation: 2,
// Indentation of the catalog files
keepRemoved: false,
// Keep keys from the catalog that are no longer in code
// You may either specify a boolean to keep or discard all removed keys.
// You may also specify an array of patterns: the keys from the catalog that are no long in the code but match one of the patterns will be kept.
// The patterns are applied to the full key including the namespace, the parent keys and the separators.
keySeparator: '.',
// Key separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
// see below for more details
lexers: {
hbs: ['HandlebarsLexer'],
handlebars: ['HandlebarsLexer'],
htm: ['HTMLLexer'],
html: ['HTMLLexer'],
mjs: ['JavascriptLexer'],
js: ['JavascriptLexer'], // if you're writing jsx inside .js files, change this to JsxLexer
ts: ['JavascriptLexer'],
jsx: ['JsxLexer'],
tsx: ['JsxLexer'],
default: ['JavascriptLexer'],
},
lineEnding: 'auto',
// Control the line ending. See options at https://github.com/ryanve/eol
locales: ['en'],
// An array of the locales in your applications
namespaceSeparator: ':',
// Namespace separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
output: 'i18n/strings.json',
// Supports $LOCALE and $NAMESPACE injection
// Supports JSON (.json) and YAML (.yml) file formats
// Where to write the locale files relative to process.cwd()
pluralSeparator: '_',
// Plural separator used in your translation keys
// If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys.
// If you don't want to generate keys for plurals (for example, in case you are using ICU format), set `pluralSeparator: false`.
input: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
// An array of globs that describe where to look for source files
// relative to the location of the configuration file
sort: false,
// Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters)
verbose: false,
// Display info about the parsing including some stats
failOnWarnings: false,
// Exit with an exit code of 1 on warnings
failOnUpdate: false,
// Exit with an exit code of 1 when translations are updated (for CI purpose)
customValueTemplate: null,
// If you wish to customize the value output the value as an object, you can set your own format.
// ${defaultValue} is the default value you set in your translation function.
// Any other custom property will be automatically extracted.
//
// Example:
// {
// message: "${defaultValue}",
// description: "${maxLength}", // t('my-key', {maxLength: 150})
// }
resetDefaultValueLocale: null,
// The locale to compare with default values to determine whether a default value has been changed.
// If this is set and a default value differs from a translation in the specified locale, all entries
// for that key across locales are reset to the default value, and existing translations are moved to
// the `_old` file.
// i18nextOptions: { returnDetails: true, lng: '$LOCALE' },
// If you wish to customize options in internally used i18next instance, you can define an object with any
// configuration property supported by i18next (https://www.i18next.com/overview/configuration-options).
// { compatibilityJSON: 'v3' } can be used to generate v3 compatible plurals.
yamlOptions: null,
// If you wish to customize options for yaml output, you can define an object here.
// Configuration options are here (https://github.com/nodeca/js-yaml#dump-object---options-).
// Example:
// {
// lineWidth: -1,
// }
};

1742
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,8 @@
"build-storybook": "storybook build",
"build-styles": "cd ./style-definitions && style-dictionary build && ./build.sh && cd -",
"test": "jest",
"format": "prettier --write **/*.{js,ts,jsx,tsx,css,md,scss}"
"format": "prettier --write **/*.{js,ts,jsx,tsx,css,md,scss}",
"translate": "i18next -c i18next-parser.config.mjs"
},
"dependencies": {
"@ant-design/icons": "4.8.3",
@ -39,10 +40,13 @@
"classnames": "2.5.1",
"date-fns": "^4.0.0",
"graphemer": "^1.4.0",
"i18next-parser": "^9.1.0",
"i18next-scanner": "^4.6.0",
"interweave": "^13.0.0",
"interweave-autolink": "^5.1.0",
"lodash": "4.17.21",
"next": "14.2.21",
"next-export-i18n": "^3.0.0",
"next-pwa": "^5.6.0",
"next-with-less": "3.0.1",
"postcss-flexbugs-fixes": "5.0.2",

View File

@ -24,7 +24,7 @@ class InlineStylesHead extends Head {
export default function Document() {
return (
<Html lang="en">
<Html>
<InlineStylesHead />
<body>
<Main />

View File

@ -5,6 +5,7 @@ import { ColumnsType } from 'antd/es/table';
import { format } from 'date-fns';
import dynamic from 'next/dynamic';
import { useTranslation } from 'next-export-i18n';
import { MessageType } from '../../../types/chat';
import {
CHAT_HISTORY,
@ -61,6 +62,7 @@ export default function Chat() {
const [bulkProcessing, setBulkProcessing] = useState(false);
const [bulkOutcome, setBulkOutcome] = useState(null);
const [bulkAction, setBulkAction] = useState('');
const { t } = useTranslation();
let outcomeTimeout = null;
let chatReloadInterval = null;
@ -153,7 +155,7 @@ export default function Chat() {
const chatColumns: ColumnsType<MessageType> = [
{
title: 'Time',
title: t('Time'),
dataIndex: 'timestamp',
key: 'timestamp',
className: 'timestamp-col',
@ -166,7 +168,7 @@ export default function Chat() {
width: 90,
},
{
title: 'User',
title: t('User'),
dataIndex: 'user',
key: 'user',
className: 'name-col',
@ -182,7 +184,7 @@ export default function Chat() {
width: 110,
},
{
title: 'Message',
title: t('Message'),
dataIndex: 'body',
key: 'body',
className: 'message-col',
@ -201,8 +203,8 @@ export default function Chat() {
key: 'hiddenAt',
className: 'toggle-col',
filters: [
{ text: 'Visible messages', value: true },
{ text: 'Hidden messages', value: false },
{ text: t('Visible messages'), value: true },
{ text: t('Hidden messages'), value: false },
],
onFilter: (value, record) => record.visible === value,
render: (hiddenAt, record) => (
@ -219,10 +221,12 @@ export default function Chat() {
return (
<div className="chat-messages">
<Title>Chat Messages</Title>
<p>Manage the messages from viewers that show up on your stream.</p>
<Title>{t('Chat Messages')}</Title>
<p>{t('Manage the messages from viewers that show up on your stream.')}</p>
<div className={bulkDivClasses}>
<span className="label">Check multiple messages to change their visibility to: </span>
<span className="label">
{t('Check multiple messages to change their visibility to:')}{' '}
</span>
<Button
type="primary"
@ -234,7 +238,7 @@ export default function Chat() {
disabled={!selectedRowKeys.length || (bulkAction && bulkAction !== 'show')}
onClick={handleSubmitBulkShow}
>
Show
{t('Show')}
</Button>
<Button
type="primary"
@ -246,7 +250,7 @@ export default function Chat() {
disabled={!selectedRowKeys.length || (bulkAction && bulkAction !== 'hide')}
onClick={handleSubmitBulkHide}
>
Hide
{t('Hide')}
</Button>
</div>
<Table

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect, useContext, ReactElement } from 'react';
import { Tabs } from 'antd';
import { useTranslation } from 'next-export-i18n';
import { ServerStatusContext } from '../../../utils/server-status-context';
import {
CONNECTED_CLIENTS,
@ -24,6 +25,7 @@ export default function ChatUsers() {
const [ipBans, setIPBans] = useState([]);
const [clients, setClients] = useState([]);
const [moderators, setModerators] = useState([]);
const { t } = useTranslation();
const getInfo = async () => {
try {
@ -71,34 +73,50 @@ export default function ChatUsers() {
<>
<ClientTable data={clients} />
<p className="description">
Visit the{' '}
{t('Visit the')}{' '}
<a
href="https://owncast.online/docs/viewers/?source=admin"
target="_blank"
rel="noopener noreferrer"
>
documentation
{t('documentation')}
</a>{' '}
to configure additional details about your viewers.
{t('to configure additional details about your viewers.')}
</p>
</>
) : (
<p className="description">
When a stream is active and chat is enabled, connected chat clients will be displayed here.
{t(
'When a stream is active and chat is enabled, connected chat clients will be displayed here.',
)}
</p>
);
const connectedUserTabTitle = (
<span>Connected {online ? `(${clients.length})` : '(offline)'}</span>
<span>
{t('Connected')} ({online ? clients.length : t('offline')})
</span>
);
const bannedUsersTabTitle = <span>Banned Users ({disabledUsers.length})</span>;
const bannedUsersTabTitle = (
<span>
{t('Banned Users')} ({disabledUsers.length})
</span>
);
const bannedUsersTable = <UserTable data={disabledUsers} />;
const bannedIPTabTitle = <span>IP Bans ({ipBans.length})</span>;
const bannedIPTabTitle = (
<span>
{t('IP Bans')} ({ipBans.length})
</span>
);
const bannedIpTable = <BannedIPsTable data={ipBans} />;
const moderatorUsersTabTitle = <span>Moderators ({moderators.length})</span>;
const moderatorUsersTabTitle = (
<span>
{t('Moderators')} ({moderators.length})
</span>
);
const moderatorTable = <UserTable data={moderators} />;
const items = [

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>

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect, useContext, ReactElement } from 'react';
import { Row, Col, Typography, MenuProps, Dropdown, Spin, Alert } from 'antd';
import { getUnixTime, sub } from 'date-fns';
import dynamic from 'next/dynamic';
import { useTranslation } from 'next-export-i18n';
import { Chart } from '../../components/admin/Chart';
import { StatisticItem } from '../../components/admin/StatisticItem';
import { ViewerTable } from '../../components/admin/ViewerTable';
@ -26,6 +27,7 @@ const FETCH_INTERVAL = 60 * 1000; // 1 min
export default function ViewersOverTime() {
const context = useContext(ServerStatusContext);
const { t } = useTranslation();
const { online, broadcaster, viewerCount, overallPeakViewerCount, sessionPeakViewerCount } =
context || {};
let streamStart;
@ -34,13 +36,13 @@ export default function ViewersOverTime() {
}
const times = [
{ title: 'Current stream', start: streamStart },
{ title: 'Last 12 hours', start: sub(new Date(), { hours: 12 }) },
{ title: 'Last 24 hours', start: sub(new Date(), { hours: 24 }) },
{ title: 'Last 7 days', start: sub(new Date(), { days: 7 }) },
{ title: 'Last 30 days', start: sub(new Date(), { days: 30 }) },
{ title: 'Last 3 months', start: sub(new Date(), { months: 3 }) },
{ title: 'Last 6 months', start: sub(new Date(), { months: 6 }) },
{ title: t('Current stream'), start: streamStart },
{ title: t('Last 12 hours'), start: sub(new Date(), { hours: 12 }) },
{ title: t('Last 24 hours'), start: sub(new Date(), { hours: 24 }) },
{ title: t('Last 7 days'), start: sub(new Date(), { days: 7 }) },
{ title: t('Last 30 days'), start: sub(new Date(), { days: 30 }) },
{ title: t('Last 3 months'), start: sub(new Date(), { months: 3 }) },
{ title: t('Last 6 months'), start: sub(new Date(), { months: 6 }) },
];
const [loadingChart, setLoadingChart] = useState(true);
@ -94,13 +96,13 @@ export default function ViewersOverTime() {
return (
<>
<Typography.Title>Viewer Info</Typography.Title>
<Typography.Title>{t('Viewer Info')}</Typography.Title>
<br />
<Row gutter={[16, 16]} justify="space-around">
{online && (
<Col span={8} md={8}>
<StatisticItem
title="Current viewers"
title={t('Current viewers')}
value={viewerCount.toString()}
prefix={<UserOutlined />}
/>
@ -108,14 +110,14 @@ export default function ViewersOverTime() {
)}
<Col md={online ? 8 : 12}>
<StatisticItem
title={online ? 'Max viewers this stream' : 'Max viewers last stream'}
title={online ? t('Max viewers this stream') : t('Max viewers last stream')}
value={sessionPeakViewerCount.toString()}
prefix={<UserOutlined />}
/>
</Col>
<Col md={online ? 8 : 12}>
<StatisticItem
title="All-time max viewers"
title={t('max viewers')}
value={overallPeakViewerCount.toString()}
prefix={<UserOutlined />}
/>
@ -125,8 +127,8 @@ export default function ViewersOverTime() {
<Alert
style={{ marginTop: '10px' }}
banner
message="Please wait"
description="No viewer data has been collected yet."
message={t('Please wait')}
description={t('No viewer data has been collected yet.')}
type="info"
/>
)}
@ -134,7 +136,7 @@ export default function ViewersOverTime() {
<Spin spinning={!viewerInfo.length || loadingChart}>
{viewerInfo.length > 0 && (
<Chart
title="Viewers"
title={t('Viewers')}
data={viewerInfo}
color="#2087E2"
unit="viewers"