Update localization files + references (#4556)
* Initial plan * Add localization support to NameChangeModal component Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Add NameChangeModal translations to English language file Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * fix(i18n): fix localization keys * chore(test): add i18n test * chore(i18n): update translation script * chore(i18n): reorgnize translation keys and update components * chore: fix linting warnings * chore(i18n): update all the language files * feat(i18n): add last live ago i18n key --------- 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>
This commit is contained in:
co-authored by
gabek
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Gabe Kangas
parent
401b5897e2
commit
1cf923a5af
@@ -4,6 +4,7 @@ import Linkify from 'react-linkify';
|
||||
import { SortOrder, TablePaginationConfig } from 'antd/lib/table/interface';
|
||||
import { format } from 'date-fns';
|
||||
import { useTranslation } from 'next-export-i18n';
|
||||
import { Localization } from '../../types/localization';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
@@ -19,10 +20,6 @@ function renderColumnLevel(text, entry) {
|
||||
return <Tag color={color}>{text}</Tag>;
|
||||
}
|
||||
|
||||
function renderMessage(text) {
|
||||
return <Linkify>{text}</Linkify>;
|
||||
}
|
||||
|
||||
export type LogTableProps = {
|
||||
logs: object[];
|
||||
initialPageSize: number;
|
||||
@@ -42,49 +39,50 @@ export const LogTable: FC<LogTableProps> = ({ logs, initialPageSize }) => {
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('Level'),
|
||||
title: t(Localization.Admin.LogTable.level),
|
||||
dataIndex: 'level',
|
||||
key: 'level',
|
||||
filters: [
|
||||
{
|
||||
text: t('Info'),
|
||||
text: t(Localization.Admin.LogTable.info),
|
||||
value: 'info',
|
||||
},
|
||||
{
|
||||
text: t('Warning'),
|
||||
text: t(Localization.Admin.LogTable.warning),
|
||||
value: 'warning',
|
||||
},
|
||||
{
|
||||
text: t('Error'),
|
||||
value: 'Error',
|
||||
text: t(Localization.Admin.LogTable.error),
|
||||
value: 'error',
|
||||
},
|
||||
],
|
||||
onFilter: (level, row) => row.level.indexOf(level) === 0,
|
||||
onFilter: (level, row) => row.level === level,
|
||||
render: renderColumnLevel,
|
||||
},
|
||||
{
|
||||
title: t('Timestamp'),
|
||||
title: t(Localization.Admin.LogTable.timestamp),
|
||||
dataIndex: 'time',
|
||||
key: 'time',
|
||||
render: timestamp => {
|
||||
render: (timestamp: Date) => {
|
||||
const dateObject = new Date(timestamp);
|
||||
return format(dateObject, 'pp P');
|
||||
return format(dateObject, 'p P');
|
||||
},
|
||||
sorter: (a, b) => new Date(a.time).getTime() - new Date(b.time).getTime(),
|
||||
sorter: (a: any, b: any) => new Date(a.time).getTime() - new Date(b.time).getTime(),
|
||||
sortDirections: ['descend', 'ascend'] as SortOrder[],
|
||||
defaultSortOrder: 'descend' as SortOrder,
|
||||
},
|
||||
|
||||
{
|
||||
title: t('Message'),
|
||||
title: t(Localization.Admin.LogTable.message),
|
||||
dataIndex: 'message',
|
||||
key: 'message',
|
||||
render: renderMessage,
|
||||
render: (message: string) => <Linkify>{message}</Linkify>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="logs-section">
|
||||
<Title>{t('Logs')}</Title>
|
||||
<Title>{t(Localization.Admin.LogTable.logs)}</Title>
|
||||
<Table
|
||||
size="middle"
|
||||
dataSource={logs}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Collapse, Typography, Skeleton } from 'antd';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import { useTranslation } from 'next-export-i18n';
|
||||
import { Localization } from '../../types/localization';
|
||||
import { fetchExternalData } from '../../utils/apis';
|
||||
|
||||
const { Panel } = Collapse;
|
||||
@@ -38,7 +39,7 @@ const ArticleItem: FC<ArticleProps> = ({
|
||||
<p className="timestamp">
|
||||
{dateString} (
|
||||
<Link href={`${OWNCAST_BASE_URL}${url}`} target="_blank" rel="noopener noreferrer">
|
||||
{t('Link')}
|
||||
{t(Localization.Admin.NewsFeed.link)}
|
||||
</Link>
|
||||
)
|
||||
</p>
|
||||
@@ -72,11 +73,12 @@ export const NewsFeed = () => {
|
||||
}, []);
|
||||
|
||||
const loadingSpinner = loading ? <Skeleton loading active /> : null;
|
||||
const noNews = !loading && feed.length === 0 ? <div>{t('No news.')}</div> : null;
|
||||
const noNews =
|
||||
!loading && feed.length === 0 ? <div>{t(Localization.Admin.NewsFeed.noNews)}</div> : null;
|
||||
|
||||
return (
|
||||
<section className="news-feed form-module">
|
||||
<Title level={2}>{t('News & Updates from Owncast')}</Title>
|
||||
<Title level={2}>{t(Localization.Admin.NewsFeed.title)}</Title>
|
||||
{loadingSpinner}
|
||||
{feed.map(item => (
|
||||
<ArticleItem {...item} key={item.url} defaultOpen={feed.length === 1} />
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React, { CSSProperties, FC, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Input, Button, Select, Form } from 'antd';
|
||||
import { useTranslation } from 'next-export-i18n';
|
||||
import { MessageType } from '../../../interfaces/socket-events';
|
||||
import WebsocketService from '../../../services/websocket-service';
|
||||
import { websocketServiceAtom, currentUserAtom } from '../../stores/ClientConfigStore';
|
||||
import { validateDisplayName } from '../../../utils/displayNameValidation';
|
||||
import { Translation } from '../../ui/Translation/Translation';
|
||||
import { Localization } from '../../../types/localization';
|
||||
import styles from './NameChangeModal.module.scss';
|
||||
|
||||
const { Option } = Select;
|
||||
@@ -28,6 +31,7 @@ type NameChangeModalProps = {
|
||||
};
|
||||
|
||||
export const NameChangeModal: FC<NameChangeModalProps> = ({ closeModal }) => {
|
||||
const { t } = useTranslation();
|
||||
const currentUser = useRecoilValue(currentUserAtom);
|
||||
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
||||
const [newName, setNewName] = useState<string>(currentUser?.displayName || '');
|
||||
@@ -70,11 +74,22 @@ export const NameChangeModal: FC<NameChangeModalProps> = ({ closeModal }) => {
|
||||
websocketService.send(colorChange);
|
||||
};
|
||||
|
||||
const showCount = info => (info.count > characterLimit ? 'Over limit' : '');
|
||||
const showCount = info =>
|
||||
info.count > characterLimit ? (
|
||||
<Translation
|
||||
translationKey={Localization.Frontend.NameChangeModal.overLimit}
|
||||
defaultText="Over limit"
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
|
||||
const maxColor = 8; // 0...n
|
||||
const colorOptions = [...Array(maxColor)].map((_, i) => i);
|
||||
|
||||
const placeholderText =
|
||||
t(Localization.Frontend.NameChangeModal.placeholder) || 'Your chat display name';
|
||||
|
||||
const validation = validateDisplayName(newName, displayName, characterLimit);
|
||||
|
||||
const saveButton = (
|
||||
@@ -84,14 +99,20 @@ export const NameChangeModal: FC<NameChangeModalProps> = ({ closeModal }) => {
|
||||
onClick={handleNameChange}
|
||||
disabled={!saveEnabled()}
|
||||
>
|
||||
Change name
|
||||
<Translation
|
||||
translationKey={Localization.Frontend.NameChangeModal.buttonText}
|
||||
defaultText="Change name"
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id="owncast-name-change-description-text">
|
||||
Your chat display name is what people see when you send chat messages.
|
||||
<Translation
|
||||
translationKey={Localization.Frontend.NameChangeModal.description}
|
||||
defaultText="Your chat display name is what people see when you send chat messages."
|
||||
/>
|
||||
</div>
|
||||
<Form onSubmitCapture={handleNameChange} className={styles.form}>
|
||||
<Input.Search
|
||||
@@ -99,8 +120,8 @@ export const NameChangeModal: FC<NameChangeModalProps> = ({ closeModal }) => {
|
||||
id="name-change-field"
|
||||
value={newName}
|
||||
onChange={e => setNewName(e.target.value)}
|
||||
placeholder="Your chat display name"
|
||||
aria-label="Your chat display name"
|
||||
placeholder={placeholderText}
|
||||
aria-label={placeholderText}
|
||||
showCount={{ formatter: showCount }}
|
||||
defaultValue={displayName}
|
||||
className={styles.inputGroup}
|
||||
@@ -109,7 +130,15 @@ export const NameChangeModal: FC<NameChangeModalProps> = ({ closeModal }) => {
|
||||
<div className={styles.error}>{validation.errorMessage}</div>
|
||||
)}
|
||||
</Form>
|
||||
<Form.Item label="Your Color" className={styles.colorChange}>
|
||||
<Form.Item
|
||||
label={
|
||||
<Translation
|
||||
translationKey={Localization.Frontend.NameChangeModal.colorLabel}
|
||||
defaultText="Your Color"
|
||||
/>
|
||||
}
|
||||
className={styles.colorChange}
|
||||
>
|
||||
<Select
|
||||
style={{ width: 120 }}
|
||||
onChange={handleColorChange}
|
||||
@@ -124,8 +153,10 @@ export const NameChangeModal: FC<NameChangeModalProps> = ({ closeModal }) => {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<div id="owncast-name-change-auth-info-text">
|
||||
You can also authenticate an IndieAuth or Fediverse account via the "Authenticate"
|
||||
menu.
|
||||
<Translation
|
||||
translationKey={Localization.Frontend.NameChangeModal.authInfo}
|
||||
defaultText='You can also authenticate an IndieAuth or Fediverse account via the "Authenticate" menu.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,13 +22,13 @@ export const Footer: FC = () => {
|
||||
</span>
|
||||
<span className={styles.links}>
|
||||
<a href="https://owncast.online/docs" target="_blank" rel="noreferrer">
|
||||
{t('Documentation')}
|
||||
{t(Localization.Frontend.Footer.documentation)}
|
||||
</a>
|
||||
<a href="https://owncast.online/help" target="_blank" rel="noreferrer">
|
||||
{t('Contribute')}
|
||||
{t(Localization.Frontend.Footer.contribute)}
|
||||
</a>
|
||||
<a href="https://github.com/owncast/owncast" target="_blank" rel="noreferrer">
|
||||
{t('Source')}
|
||||
{t(Localization.Frontend.Footer.source)}
|
||||
</a>
|
||||
</span>
|
||||
</footer>
|
||||
|
||||
@@ -4,6 +4,7 @@ import cn from 'classnames';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Link from 'next/link';
|
||||
import { useTranslation } from 'next-export-i18n';
|
||||
import { Localization } from '../../../types/localization';
|
||||
import styles from './Header.module.scss';
|
||||
|
||||
// Lazy loaded components
|
||||
@@ -34,18 +35,18 @@ export const Header: FC<HeaderComponentProps> = ({ name, chatAvailable, chatDisa
|
||||
<header className={cn([`${styles.header}`], 'global-header')}>
|
||||
{online ? (
|
||||
<Link href="#player" className={styles.skipLink}>
|
||||
{t('Skip to player')}
|
||||
{t(Localization.Frontend.Header.skipToPlayer)}
|
||||
</Link>
|
||||
) : (
|
||||
<Link href="#offline-message" className={styles.skipLink}>
|
||||
{t('Skip to offline message')}
|
||||
{t(Localization.Frontend.Header.skipToOfflineMessage)}
|
||||
</Link>
|
||||
)}
|
||||
<Link href="#skip-to-content" className={styles.skipLink}>
|
||||
{t('Skip to page content')}
|
||||
{t(Localization.Frontend.Header.skipToContent)}
|
||||
</Link>
|
||||
<Link href="#footer" className={styles.skipLink}>
|
||||
{t('Skip to footer')}
|
||||
{t(Localization.Frontend.Header.skipToFooter)}
|
||||
</Link>
|
||||
<div className={styles.logo}>
|
||||
<div id="header-logo" className={styles.logoImage}>
|
||||
@@ -61,11 +62,11 @@ export const Header: FC<HeaderComponentProps> = ({ name, chatAvailable, chatDisa
|
||||
{!chatAvailable && !chatDisabled && (
|
||||
<Tooltip
|
||||
overlayClassName={styles.toolTip}
|
||||
title={t('Chat will be available when the stream is live.')}
|
||||
title={t(Localization.Frontend.Header.chatWillBeAvailable)}
|
||||
placement="left"
|
||||
>
|
||||
<span className={styles.chatOfflineText} id="owncast-chat-offline-text">
|
||||
{t('Chat is offline')}
|
||||
{t(Localization.Frontend.Header.chatOffline)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -112,7 +112,7 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
|
||||
<div className={styles.lastLiveDate}>
|
||||
<ClockCircleOutlined className={styles.clockIcon} />
|
||||
<span id="owncast-offline-last-live-text">
|
||||
{`${t('Last live ago', { timeAgo: formatDistanceToNow(new Date(lastLive)) })}`}
|
||||
{`${t(Localization.Frontend.lastLiveAgo, { timeAgo: formatDistanceToNow(new Date(lastLive)) })}`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user