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:
Copilot
2025-09-15 19:27:56 -07:00
committed by GitHub
co-authored by gabek copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Gabe Kangas
parent 401b5897e2
commit 1cf923a5af
46 changed files with 6053 additions and 4639 deletions
+15 -17
View File
@@ -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 -3
View File
@@ -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 &quot;Authenticate&quot;
menu.
<Translation
translationKey={Localization.Frontend.NameChangeModal.authInfo}
defaultText='You can also authenticate an IndieAuth or Fediverse account via the "Authenticate" menu.'
/>
</div>
</div>
);
+3 -3
View File
@@ -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>
+7 -6
View File
@@ -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>
)}