@linktr.ee/messaging-react 1.40.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -101,6 +101,11 @@ export declare interface ChannelListProps {
101
101
  sort?: ChannelSort;
102
102
  customEmptyStateIndicator?: React.ComponentType<EmptyStateIndicatorProps>;
103
103
  renderMessagePreview?: (message: LocalMessage | undefined, defaultPreview?: string) => React.ReactNode;
104
+ /**
105
+ * Language code used to pick translated message text from Stream Chat i18n.
106
+ * Falls back to message.text when no matching translation exists.
107
+ */
108
+ viewerLanguage?: string;
104
109
  }
105
110
 
106
111
  /**
@@ -112,7 +117,7 @@ export declare const ChannelView: default_2.NamedExoticComponent<ChannelViewProp
112
117
  * Props that MessagingShell passes through to ChannelView.
113
118
  * ChannelViewProps is the source of truth for these props.
114
119
  */
115
- declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onDeleteConversationClick' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'messageMetadata' | 'onMessageSent' | 'showStarButton' | 'chatbotVotingEnabled' | 'renderChannelBanner' | 'customProfileContent' | 'customChannelActions' | 'renderMessage' | 'sendButton'>;
120
+ declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onDeleteConversationClick' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'messageMetadata' | 'onMessageSent' | 'showStarButton' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderChannelBanner' | 'customProfileContent' | 'customChannelActions' | 'renderMessage' | 'sendButton'>;
116
121
 
117
122
  /**
118
123
  * ChannelView component props
@@ -185,6 +190,11 @@ export declare interface ChannelViewProps {
185
190
  * Defaults to false.
186
191
  */
187
192
  chatbotVotingEnabled?: boolean;
193
+ /**
194
+ * Language code used to pick translated message text from Stream Chat i18n.
195
+ * Falls back to message.text when no matching translation exists.
196
+ */
197
+ viewerLanguage?: string;
188
198
  /**
189
199
  * Custom render function for a banner/card component that renders
190
200
  * between the channel header and message list.
@@ -283,6 +293,11 @@ export declare interface FaqListProps {
283
293
  */
284
294
  export declare const formatRelativeTime: (date: Date) => string;
285
295
 
296
+ export declare function getMessageDisplayText({ message, viewerLanguage, }: {
297
+ message?: MessageWithI18n | null;
298
+ viewerLanguage?: string;
299
+ }): string | undefined;
300
+
286
301
  export declare function isLinkAttachment(a: Attachment): boolean;
287
302
 
288
303
  export declare const LockedAttachment: {
@@ -358,6 +373,10 @@ declare interface MessageVoteButtonsProps {
358
373
  onVoteDown: () => void;
359
374
  }
360
375
 
376
+ declare type MessageWithI18n = Pick<LocalMessage, 'text'> & {
377
+ i18n?: Record<string, string> | null;
378
+ };
379
+
361
380
  /**
362
381
  * Messaging capabilities configuration
363
382
  */
@@ -462,13 +481,14 @@ export declare interface MessagingShellProps extends ChannelViewPassthroughProps
462
481
  renderMessagePreview?: (message: LocalMessage | undefined, defaultPreview?: string) => React.ReactNode;
463
482
  }
464
483
 
484
+ export declare function normalizeLanguageCode(language?: string): string | undefined;
485
+
465
486
  /**
466
487
  * Generic participant interface for different host environments
467
488
  */
468
489
  export declare interface Participant {
469
490
  id: string;
470
491
  name: string;
471
- email?: string;
472
492
  image?: string;
473
493
  username?: string;
474
494
  phone?: string;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { b as e, c as t, C as i, d as n, e as o, f as g, F as m, g as r, L as M, M as l, h, i as u, j as L, k as c, l as d, r as v, m as C, u as A, n as k, o as F } from "./index-B_PLgcDi.js";
1
+ import { b as e, c as t, C as i, d as n, e as o, f as g, F as m, g as r, L as M, M as l, h as u, i as h, j as L, k as d, l as C, m as c, n as v, r as A, o as k, u as p, p as F, q as f } from "./index-Brz9orsI.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
@@ -10,15 +10,17 @@ export {
10
10
  r as FaqListItem,
11
11
  M as LockedAttachment,
12
12
  l as MediaMessage,
13
- h as MessageVoteButtons,
14
- u as MessagingProvider,
13
+ u as MessageVoteButtons,
14
+ h as MessagingProvider,
15
15
  L as MessagingShell,
16
- c as formatRelativeTime,
17
- d as isLinkAttachment,
18
- v as resolveLinkAttachment,
19
- C as resolveMediaFromMessage,
20
- A as useCustomMessage,
21
- k as useMessageVote,
22
- F as useMessaging
16
+ d as formatRelativeTime,
17
+ C as getMessageDisplayText,
18
+ c as isLinkAttachment,
19
+ v as normalizeLanguageCode,
20
+ A as resolveLinkAttachment,
21
+ k as resolveMediaFromMessage,
22
+ p as useCustomMessage,
23
+ F as useMessageVote,
24
+ f as useMessaging
23
25
  };
24
26
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "1.40.2",
3
+ "version": "2.0.0",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@linktr.ee/component-library": "11.8.6",
37
- "@linktr.ee/messaging-core": "^1.8.0",
37
+ "@linktr.ee/messaging-core": "^2.0.0",
38
38
  "@phosphor-icons/react": "^2.1.10"
39
39
  },
40
40
  "devDependencies": {
@@ -65,14 +65,13 @@ const createChannel = () =>
65
65
  }) as unknown as Channel
66
66
 
67
67
  const createParticipant = (
68
- overrides: Partial<{ name: string; email: string; username: string }> = {}
68
+ overrides: Partial<{ name: string; username: string }> = {}
69
69
  ) =>
70
70
  ({
71
71
  user: {
72
72
  id: 'linker-1',
73
73
  name: overrides.name ?? 'Linker',
74
74
  image: undefined,
75
- email: overrides.email,
76
75
  username: overrides.username,
77
76
  },
78
77
  role: 'member',
@@ -99,18 +98,7 @@ describe('ChannelInfoDialog', () => {
99
98
  expect(nameEl).toBeInTheDocument()
100
99
  })
101
100
 
102
- it('renders participant email as secondary info', () => {
103
- renderWithProviders(
104
- <ChannelInfoDialog
105
- {...defaultProps()}
106
- participant={createParticipant({ email: 'linker@example.com' })}
107
- />
108
- )
109
-
110
- expect(screen.getByText('linker@example.com')).toBeInTheDocument()
111
- })
112
-
113
- it('renders participant username as secondary info when no email', () => {
101
+ it('renders participant username as secondary info', () => {
114
102
  renderWithProviders(
115
103
  <ChannelInfoDialog
116
104
  {...defaultProps()}
@@ -12,9 +12,8 @@ import ActionButton from '../ActionButton'
12
12
  import { Avatar } from '../Avatar'
13
13
  import { CloseButton } from '../CloseButton'
14
14
 
15
- // Custom user type with email and username
15
+ // Custom user type with username
16
16
  type CustomUser = {
17
- email?: string
18
17
  username?: string
19
18
  }
20
19
 
@@ -180,13 +179,10 @@ export const ChannelInfoDialog: React.FC<ChannelInfoDialogProps> = ({
180
179
  const participantName =
181
180
  participant.user?.name || participant.user?.id || 'Unknown member'
182
181
  const participantImage = participant.user?.image
183
- const participantEmail = (participant.user as CustomUser)?.email
184
182
  const participantUsername = (participant.user as CustomUser)?.username
185
- const participantSecondary = participantEmail
186
- ? participantEmail
187
- : participantUsername
188
- ? `linktr.ee/${participantUsername}`
189
- : undefined
183
+ const participantSecondary = participantUsername
184
+ ? `linktr.ee/${participantUsername}`
185
+ : undefined
190
186
  const participantId = participant.user?.id || 'unknown'
191
187
 
192
188
  return (
@@ -9,6 +9,7 @@ type ChannelListContextValue = {
9
9
  message: LocalMessage | undefined,
10
10
  defaultPreview?: string
11
11
  ) => React.ReactNode
12
+ viewerLanguage?: string
12
13
  }
13
14
 
14
15
  const ChannelListContext = React.createContext<ChannelListContextValue>({
@@ -16,6 +17,7 @@ const ChannelListContext = React.createContext<ChannelListContextValue>({
16
17
  onChannelSelect: () => {},
17
18
  debug: false,
18
19
  renderMessagePreview: undefined,
20
+ viewerLanguage: undefined,
19
21
  })
20
22
 
21
23
  export const ChannelListProvider = ChannelListContext.Provider
@@ -4,6 +4,7 @@ import { ChannelPreviewUIComponentProps } from 'stream-chat-react'
4
4
 
5
5
  import { useChannelStar } from '../../hooks/useChannelStar'
6
6
  import { formatRelativeTime } from '../../utils/formatRelativeTime'
7
+ import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
7
8
  import { Avatar } from '../Avatar'
8
9
  import { isChatbotMessage } from '../CustomMessage/MessageTag'
9
10
 
@@ -14,8 +15,13 @@ import { useChannelListContext } from './ChannelListContext'
14
15
  */
15
16
  const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
16
17
  ({ channel, unread }) => {
17
- const { selectedChannel, onChannelSelect, debug, renderMessagePreview } =
18
- useChannelListContext()
18
+ const {
19
+ selectedChannel,
20
+ onChannelSelect,
21
+ debug,
22
+ renderMessagePreview,
23
+ viewerLanguage,
24
+ } = useChannelListContext()
19
25
 
20
26
  const isSelected = selectedChannel?.id === channel?.id
21
27
 
@@ -53,7 +59,12 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
53
59
  })()
54
60
 
55
61
  const getLastMessageText = () => {
56
- if (lastMessage?.text) return lastMessage.text
62
+ const displayText = getMessageDisplayText({
63
+ message: lastMessage,
64
+ viewerLanguage,
65
+ })
66
+ if (displayText) return displayText
67
+
57
68
  const isTip = lastMessage?.metadata?.custom_type === 'MESSAGE_TIP'
58
69
  if (isTip) return '💵 Sent a tip'
59
70
 
@@ -28,6 +28,7 @@ export const ChannelList = React.memo<ChannelListProps>(
28
28
  className,
29
29
  customEmptyStateIndicator,
30
30
  renderMessagePreview,
31
+ viewerLanguage,
31
32
  }) => {
32
33
  // Track renders
33
34
  const renderCountRef = React.useRef(0)
@@ -64,8 +65,15 @@ export const ChannelList = React.memo<ChannelListProps>(
64
65
  onChannelSelect,
65
66
  debug,
66
67
  renderMessagePreview,
68
+ viewerLanguage,
67
69
  }),
68
- [selectedChannel, onChannelSelect, debug, renderMessagePreview]
70
+ [
71
+ selectedChannel,
72
+ onChannelSelect,
73
+ debug,
74
+ renderMessagePreview,
75
+ viewerLanguage,
76
+ ]
69
77
  )
70
78
 
71
79
  return (
@@ -257,6 +257,7 @@ const ChannelViewInner: React.FC<{
257
257
  message: NonNullable<MessageUIComponentProps['message']>
258
258
  ) => React.ReactNode
259
259
  dmAgentEnabled?: boolean
260
+ viewerLanguage?: string
260
261
  }> = ({
261
262
  onBack,
262
263
  showBackButton,
@@ -275,6 +276,7 @@ const ChannelViewInner: React.FC<{
275
276
  customChannelActions,
276
277
  renderMessage,
277
278
  dmAgentEnabled = false,
279
+ viewerLanguage,
278
280
  }) => {
279
281
  const { channel } = useChannelStateContext()
280
282
  const infoDialogRef = useRef<HTMLDialogElement>(null)
@@ -339,7 +341,11 @@ const ChannelViewInner: React.FC<{
339
341
  // eslint-disable-next-line react-hooks/rules-of-hooks
340
342
  const { message } = useMessageContext('ChannelView')
341
343
  const messageNode = (
342
- <CustomMessage {...props} chatbotVotingEnabled={chatbotVotingEnabled} />
344
+ <CustomMessage
345
+ {...props}
346
+ chatbotVotingEnabled={chatbotVotingEnabled}
347
+ viewerLanguage={viewerLanguage}
348
+ />
343
349
  )
344
350
 
345
351
  if (!renderMessage || !message) {
@@ -348,7 +354,7 @@ const ChannelViewInner: React.FC<{
348
354
 
349
355
  return renderMessage(messageNode, message)
350
356
  },
351
- [chatbotVotingEnabled, renderMessage]
357
+ [chatbotVotingEnabled, renderMessage, viewerLanguage]
352
358
  )
353
359
 
354
360
  return (
@@ -436,6 +442,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
436
442
  customChannelActions,
437
443
  renderMessage,
438
444
  sendButton,
445
+ viewerLanguage,
439
446
  }) => {
440
447
  // Custom send message handler that:
441
448
  // 1. Applies messageMetadata if provided
@@ -517,6 +524,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
517
524
  customProfileContent={customProfileContent}
518
525
  customChannelActions={customChannelActions}
519
526
  renderMessage={renderMessage}
527
+ viewerLanguage={viewerLanguage}
520
528
  />
521
529
  </Channel>
522
530
  </div>
@@ -30,6 +30,7 @@ import {
30
30
  } from 'stream-chat-react'
31
31
 
32
32
  import { useMessageVote } from '../../hooks/useMessageVote'
33
+ import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
33
34
  import { Avatar } from '../Avatar'
34
35
  import LockedAttachment from '../LockedAttachment'
35
36
  import { isLinkAttachment } from '../MediaMessage'
@@ -45,10 +46,12 @@ import { MessageVoteButtons } from './MessageVoteButtons'
45
46
 
46
47
  type CustomMessageUIComponentProps = MessageUIComponentProps & {
47
48
  chatbotVotingEnabled?: boolean
49
+ viewerLanguage?: string
48
50
  }
49
51
 
50
52
  type CustomMessageWithContextProps = MessageContextValue & {
51
53
  chatbotVotingEnabled?: boolean
54
+ viewerLanguage?: string
52
55
  }
53
56
 
54
57
  const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
@@ -68,11 +71,13 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
68
71
  message,
69
72
  renderText,
70
73
  threadList,
74
+ viewerLanguage,
71
75
  } = props
72
76
 
73
77
  const { client } = useChatContext('CustomMessage')
74
78
  const { channel } = useChannelStateContext('CustomMessage')
75
- const { isUnlocking, onUnlockClick, onFetchSource, onDownloadClick } = useCustomMessage('LockedAttachment')
79
+ const { isUnlocking, onUnlockClick, onFetchSource, onDownloadClick } =
80
+ useCustomMessage('LockedAttachment')
76
81
  const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false)
77
82
  const reminder = useMessageReminder(message.id)
78
83
  const { selected: voteState, voteUp, voteDown } = useMessageVote(message)
@@ -107,6 +112,12 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
107
112
  const filtered = raw.filter((a) => !('type' in a) || !isLinkAttachment(a))
108
113
  return filtered.length === raw.length ? raw : filtered
109
114
  }, [message])
115
+ const displayMessage = useMemo(() => {
116
+ const displayText = getMessageDisplayText({ message, viewerLanguage })
117
+ return displayText === message.text
118
+ ? message
119
+ : { ...message, text: displayText }
120
+ }, [message, viewerLanguage])
110
121
 
111
122
  if (isDateSeparatorMessage(message)) {
112
123
  return null
@@ -247,7 +258,10 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
247
258
  )}
248
259
  {message.text && (
249
260
  <div className="str-chat__message-bubble">
250
- <MessageText message={message} renderText={renderText} />
261
+ <MessageText
262
+ message={displayMessage}
263
+ renderText={renderText}
264
+ />
251
265
  </div>
252
266
  )}
253
267
  </div>
@@ -273,11 +287,14 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
273
287
  ) : null}
274
288
  {isAIGenerated ? (
275
289
  <StreamedMessageText
276
- message={message}
290
+ message={displayMessage}
277
291
  renderText={renderText}
278
292
  />
279
293
  ) : (
280
- <MessageText message={message} renderText={renderText} />
294
+ <MessageText
295
+ message={displayMessage}
296
+ renderText={renderText}
297
+ />
281
298
  )}
282
299
  <MessageErrorIcon />
283
300
  </div>
@@ -317,6 +334,7 @@ const MemoizedCustomMessage = React.memo(
317
334
  CustomMessageWithContext,
318
335
  (prev, next) => {
319
336
  if (prev.chatbotVotingEnabled !== next.chatbotVotingEnabled) return false
337
+ if (prev.viewerLanguage !== next.viewerLanguage) return false
320
338
  return areMessageUIPropsEqual(prev, next)
321
339
  }
322
340
  ) as typeof CustomMessageWithContext
@@ -35,6 +35,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
35
35
  onMessageSent,
36
36
  showStarButton = false,
37
37
  chatbotVotingEnabled = false,
38
+ viewerLanguage,
38
39
  renderMessagePreview,
39
40
  renderChannelBanner,
40
41
  customProfileContent,
@@ -60,9 +61,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
60
61
  string | null
61
62
  >(null)
62
63
 
63
- const {
64
- showDeleteConversation = true,
65
- } = capabilities
64
+ const { showDeleteConversation = true } = capabilities
66
65
 
67
66
  // Create default filters and merge with provided filters
68
67
  const channelFilters = React.useMemo(() => {
@@ -194,7 +193,6 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
194
193
  const channel = await service.startChannelWithParticipant({
195
194
  id: initialParticipantData.id,
196
195
  name: initialParticipantData.name,
197
- email: initialParticipantData.email,
198
196
  phone: initialParticipantData.phone,
199
197
  })
200
198
 
@@ -375,6 +373,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
375
373
  channelRenderFilterFn={channelRenderFilterFn}
376
374
  customEmptyStateIndicator={channelListCustomEmptyStateIndicator}
377
375
  renderMessagePreview={renderMessagePreview}
376
+ viewerLanguage={viewerLanguage}
378
377
  />
379
378
  </div>
380
379
 
@@ -418,6 +417,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
418
417
  onMessageSent={onMessageSent}
419
418
  showStarButton={showStarButton}
420
419
  chatbotVotingEnabled={chatbotVotingEnabled}
420
+ viewerLanguage={viewerLanguage}
421
421
  customProfileContent={customProfileContent}
422
422
  customChannelActions={customChannelActions}
423
423
  renderMessage={renderMessage}
package/src/index.ts CHANGED
@@ -18,7 +18,10 @@ export {
18
18
  resolveLinkAttachment,
19
19
  resolveMediaFromMessage,
20
20
  } from './components/MediaMessage'
21
- export type { MediaMessageProps, MediaMessageResolved } from './components/MediaMessage'
21
+ export type {
22
+ MediaMessageProps,
23
+ MediaMessageResolved,
24
+ } from './components/MediaMessage'
22
25
 
23
26
  // Providers
24
27
  export { MessagingProvider } from './providers/MessagingProvider'
@@ -31,6 +34,10 @@ export { useCustomMessage } from './components/CustomMessage/context'
31
34
 
32
35
  // Utils
33
36
  export { formatRelativeTime } from './utils/formatRelativeTime'
37
+ export {
38
+ getMessageDisplayText,
39
+ normalizeLanguageCode,
40
+ } from './utils/getMessageDisplayText'
34
41
 
35
42
  // Types
36
43
  export type {
@@ -45,7 +52,11 @@ export type {
45
52
  export type { MessageMetadata } from './stream-custom-data'
46
53
  export type { AvatarProps } from './components/Avatar'
47
54
  export type { ActionButtonProps } from './components/ActionButton'
48
- export type { CreatorCardProps, VisitorCardProps, LockedAttachmentContextValue } from './components/LockedAttachment'
55
+ export type {
56
+ CreatorCardProps,
57
+ VisitorCardProps,
58
+ LockedAttachmentContextValue,
59
+ } from './components/LockedAttachment'
49
60
  export type { CustomMessageRegistry } from './components/CustomMessage/context'
50
61
  export type { AttachmentSourceType } from './components/AttachmentCard/utils/mimeType'
51
62
  export type { Faq, FaqListProps } from './components/FaqList'
@@ -110,31 +110,26 @@ export const mockParticipants = [
110
110
  {
111
111
  id: 'participant-1',
112
112
  name: 'Alice Johnson',
113
- email: 'alice@example.com',
114
113
  image: 'https://i.pravatar.cc/150?img=2',
115
114
  },
116
115
  {
117
116
  id: 'participant-2',
118
117
  name: 'Bob Smith',
119
- email: 'bob@example.com',
120
118
  image: 'https://i.pravatar.cc/150?img=3',
121
119
  },
122
120
  {
123
121
  id: 'participant-3',
124
122
  name: 'Carol Williams',
125
- email: 'carol@example.com',
126
123
  image: 'https://i.pravatar.cc/150?img=4',
127
124
  },
128
125
  {
129
126
  id: 'participant-4',
130
127
  name: 'David Brown',
131
- email: 'david@example.com',
132
128
  image: 'https://i.pravatar.cc/150?img=5',
133
129
  },
134
130
  {
135
131
  id: 'participant-5',
136
132
  name: 'Emma Davis',
137
- email: 'emma@example.com',
138
133
  image: 'https://i.pravatar.cc/150?img=6',
139
134
  },
140
135
  ]
@@ -143,10 +138,8 @@ export const mockParticipants = [
143
138
  export const mockParticipantSource = {
144
139
  loadParticipants: async (options?: { search?: string; limit?: number }) => {
145
140
  const searchTerm = options?.search || ''
146
- const filtered = mockParticipants.filter(
147
- (p) =>
148
- p.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
149
- p.email.toLowerCase().includes(searchTerm.toLowerCase())
141
+ const filtered = mockParticipants.filter((p) =>
142
+ p.name.toLowerCase().includes(searchTerm.toLowerCase())
150
143
  )
151
144
  return {
152
145
  participants: filtered,
package/src/types.ts CHANGED
@@ -25,7 +25,6 @@ export type { LockedAttachmentSource } from './components/LockedAttachment'
25
25
  export interface Participant {
26
26
  id: string
27
27
  name: string
28
- email?: string
29
28
  image?: string
30
29
  username?: string
31
30
  phone?: string
@@ -95,6 +94,11 @@ export interface ChannelListProps {
95
94
  message: LocalMessage | undefined,
96
95
  defaultPreview?: string
97
96
  ) => React.ReactNode
97
+ /**
98
+ * Language code used to pick translated message text from Stream Chat i18n.
99
+ * Falls back to message.text when no matching translation exists.
100
+ */
101
+ viewerLanguage?: string
98
102
  }
99
103
 
100
104
  /**
@@ -173,6 +177,11 @@ export interface ChannelViewProps {
173
177
  * Defaults to false.
174
178
  */
175
179
  chatbotVotingEnabled?: boolean
180
+ /**
181
+ * Language code used to pick translated message text from Stream Chat i18n.
182
+ * Falls back to message.text when no matching translation exists.
183
+ */
184
+ viewerLanguage?: string
176
185
 
177
186
  /**
178
187
  * Custom render function for a banner/card component that renders
@@ -244,6 +253,7 @@ export type ChannelViewPassthroughProps = Pick<
244
253
  | 'onMessageSent'
245
254
  | 'showStarButton'
246
255
  | 'chatbotVotingEnabled'
256
+ | 'viewerLanguage'
247
257
  | 'renderChannelBanner'
248
258
  | 'customProfileContent'
249
259
  | 'customChannelActions'
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ getMessageDisplayText,
5
+ normalizeLanguageCode,
6
+ } from './getMessageDisplayText'
7
+
8
+ describe('getMessageDisplayText', () => {
9
+ it('returns translated text for the viewer language', () => {
10
+ expect(
11
+ getMessageDisplayText({
12
+ message: {
13
+ text: 'Bonjour',
14
+ i18n: {
15
+ language: 'fr',
16
+ en_text: 'Hello',
17
+ },
18
+ },
19
+ viewerLanguage: 'en-US',
20
+ })
21
+ ).toBe('Hello')
22
+ })
23
+
24
+ it('falls back to the original message text when no translation exists', () => {
25
+ expect(
26
+ getMessageDisplayText({
27
+ message: {
28
+ text: 'Bonjour',
29
+ i18n: {
30
+ language: 'fr',
31
+ },
32
+ },
33
+ viewerLanguage: 'es',
34
+ })
35
+ ).toBe('Bonjour')
36
+ })
37
+ })
38
+
39
+ describe('normalizeLanguageCode', () => {
40
+ it('normalizes locale-style language codes to their primary subtag', () => {
41
+ expect(normalizeLanguageCode('fr-FR')).toBe('fr')
42
+ expect(normalizeLanguageCode('en_US')).toBe('en')
43
+ })
44
+ })
@@ -0,0 +1,27 @@
1
+ import type { LocalMessage } from 'stream-chat'
2
+
3
+ type MessageWithI18n = Pick<LocalMessage, 'text'> & {
4
+ i18n?: Record<string, string> | null
5
+ }
6
+
7
+ export function normalizeLanguageCode(language?: string): string | undefined {
8
+ const normalized = language?.trim().toLowerCase().split(/[-_]/)[0]
9
+ return normalized || undefined
10
+ }
11
+
12
+ export function getMessageDisplayText({
13
+ message,
14
+ viewerLanguage,
15
+ }: {
16
+ message?: MessageWithI18n | null
17
+ viewerLanguage?: string
18
+ }): string | undefined {
19
+ const fallbackText = message?.text
20
+ const normalizedLanguage = normalizeLanguageCode(viewerLanguage)
21
+
22
+ if (!normalizedLanguage) {
23
+ return fallbackText
24
+ }
25
+
26
+ return message?.i18n?.[`${normalizedLanguage}_text`] ?? fallbackText
27
+ }