Add localization support for chat message components with enhanced Translation component (#4560)

* Initial plan

* Add localization support for chat message components

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Update chat message localization to use string interpolation

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Restore original styling while preserving string interpolation

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Separate styling from variables in translation templates

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Revert to concatenation approach for chat message localization

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Add id property to Translation component and restore missing CSS classes and IDs

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Javascript formatting autofixes

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: gabek <414923+gabek@users.noreply.github.com>
Co-authored-by: Gabe Kangas <gabek@real-ity.com>
Co-authored-by: Owncast <owncast@owncast.online>
This commit is contained in:
Copilot
2025-09-16 00:13:13 -07:00
committed by GitHub
co-authored by gabek copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Gabe Kangas Owncast
parent fa1b07e987
commit 275339bdf0
7 changed files with 55 additions and 12 deletions
@@ -1,6 +1,8 @@
import { FC } from 'react'; import { FC } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { ModerationBadge } from '../ChatUserBadge/ModerationBadge'; import { ModerationBadge } from '../ChatUserBadge/ModerationBadge';
import { Translation } from '../../ui/Translation/Translation';
import { Localization } from '../../../types/localization';
import styles from './ChatJoinMessage.module.scss'; import styles from './ChatJoinMessage.module.scss';
@@ -29,13 +31,19 @@ export const ChatJoinMessage: FC<ChatJoinMessageProps> = ({
<span className={styles.icon}> <span className={styles.icon}>
<UsergroupAddOutlined /> <UsergroupAddOutlined />
</span> </span>
<span className={styles.user}>{displayName}</span>
{isAuthorModerator && ( {isAuthorModerator && (
<span className={styles.moderatorBadge}> <span className={styles.moderatorBadge}>
<ModerationBadge userColor={userColor} /> <ModerationBadge userColor={userColor} />
</span> </span>
)} )}
<span className={styles.joinMessage}>joined the chat.</span> <span className={styles.joinMessage}>
<span className={styles.user}>{displayName}</span>
<span> </span>
<Translation
translationKey={Localization.Frontend.Chat.userJoined}
defaultText="joined the chat."
/>
</span>
</span> </span>
</div> </div>
); );
@@ -1,9 +1,14 @@
import { Translation } from '../../ui/Translation/Translation';
import { Localization } from '../../../types/localization';
import styles from './ChatModeratorNotification.module.scss'; import styles from './ChatModeratorNotification.module.scss';
import Icon from '../../../assets/images/moderator.svg'; import Icon from '../../../assets/images/moderator.svg';
export const ChatModeratorNotification = () => ( export const ChatModeratorNotification = () => (
<div className={styles.chatModerationNotification}> <div className={styles.chatModerationNotification}>
<Icon className={styles.icon} /> <Icon className={styles.icon} />
You are now a moderator. <Translation
translationKey={Localization.Frontend.Chat.moderatorNotification}
defaultText="You are now a moderator."
/>
</div> </div>
); );
@@ -1,8 +1,8 @@
// export const ChatSocialMessage: FC<ChatSocialMessageProps> = ({ message }) => {
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { FC } from 'react'; import { FC } from 'react';
import { NameChangeEvent } from '../../../interfaces/socket-events'; import { NameChangeEvent } from '../../../interfaces/socket-events';
import { Translation } from '../../ui/Translation/Translation';
import { Localization } from '../../../types/localization';
import styles from './ChatNameChangeMessage.module.scss'; import styles from './ChatNameChangeMessage.module.scss';
export interface ChatNameChangeMessageProps { export interface ChatNameChangeMessageProps {
@@ -27,10 +27,14 @@ export const ChatNameChangeMessage: FC<ChatNameChangeMessageProps> = ({ message
</div> </div>
<div className={styles.nameChangeText}> <div className={styles.nameChangeText}>
<span style={{ color }}>{oldName}</span> <span style={{ color }}>{oldName}</span>
<span className={styles.plain} id="owncast-name-change-is-now-known-text"> <span> </span>
{' '} <Translation
is now known as{' '} translationKey={Localization.Frontend.Chat.nameChangeText}
</span> className={styles.plain}
id="owncast-name-change-is-now-known-text"
defaultText="is now known as"
/>
<span> </span>
<span style={{ color }}>{displayName}</span> <span style={{ color }}>{displayName}</span>
</div> </div>
</div> </div>
@@ -1,6 +1,8 @@
import { FC } from 'react'; import { FC } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { ModerationBadge } from '../ChatUserBadge/ModerationBadge'; import { ModerationBadge } from '../ChatUserBadge/ModerationBadge';
import { Translation } from '../../ui/Translation/Translation';
import { Localization } from '../../../types/localization';
import styles from './ChatPartMessage.module.scss'; import styles from './ChatPartMessage.module.scss';
@@ -29,13 +31,19 @@ export const ChatPartMessage: FC<ChatPartMessageProps> = ({
<span className={styles.icon}> <span className={styles.icon}>
<UsergroupDeleteOutlined /> <UsergroupDeleteOutlined />
</span> </span>
<span className={styles.user}>{displayName}</span>
{isAuthorModerator && ( {isAuthorModerator && (
<span className={styles.moderatorBadge}> <span className={styles.moderatorBadge}>
<ModerationBadge userColor={userColor} /> <ModerationBadge userColor={userColor} />
</span> </span>
)} )}
<span className={styles.partMessage}>left the chat.</span> <span className={styles.partMessage}>
<span className={styles.user}>{displayName}</span>
<span> </span>
<Translation
translationKey={Localization.Frontend.Chat.userLeft}
defaultText="left the chat."
/>
</span>
</span> </span>
</div> </div>
); );
@@ -7,6 +7,7 @@ export interface TranslationProps {
translationKey: LocalizationKey; translationKey: LocalizationKey;
vars?: Record<string, any>; vars?: Record<string, any>;
className?: string; className?: string;
id?: string;
defaultText?: string; defaultText?: string;
count?: number; count?: number;
} }
@@ -15,6 +16,7 @@ export const Translation: FC<TranslationProps> = ({
translationKey, translationKey,
vars, vars,
className, className,
id,
defaultText, defaultText,
count, count,
}) => { }) => {
@@ -55,7 +57,9 @@ export const Translation: FC<TranslationProps> = ({
} }
} }
return <span className={className} dangerouslySetInnerHTML={{ __html: translatedText }} />; return (
<span className={className} id={id} dangerouslySetInnerHTML={{ __html: translatedText }} />
);
}; };
export default Translation; export default Translation;
+6
View File
@@ -118,6 +118,12 @@
"contribute": "Contribute", "contribute": "Contribute",
"source": "Source" "source": "Source"
}, },
"Chat": {
"userJoined": "joined the chat.",
"userLeft": "left the chat.",
"nameChangeText": "is now known as",
"moderatorNotification": "You are now a moderator."
,
"FollowModal": { "FollowModal": {
"description": "By following this stream you'll get notified on the Fediverse when it goes live. Now is a great time to", "description": "By following this stream you'll get notified on the Fediverse when it goes live. Now is a great time to",
"learnFediverse": "learn about the Fediverse", "learnFediverse": "learn about the Fediverse",
+8
View File
@@ -100,6 +100,14 @@ export const Localization = {
source: 'Frontend.Footer.source', source: 'Frontend.Footer.source',
}, },
// Chat message components
Chat: {
userJoined: 'Frontend.Chat.userJoined',
userLeft: 'Frontend.Chat.userLeft',
nameChangeText: 'Frontend.Chat.nameChangeText',
moderatorNotification: 'Frontend.Chat.moderatorNotification',
},
// Follow modal component // Follow modal component
FollowModal: { FollowModal: {
description: 'Frontend.FollowModal.description', description: 'Frontend.FollowModal.description',