stream-chat-react-native-core 9.11.3-beta.1 → 9.12.0-beta.1

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.
Files changed (114) hide show
  1. package/lib/commonjs/components/ChannelPreview/hooks/useChannelPreviewPollLabel.js +8 -5
  2. package/lib/commonjs/components/ChannelPreview/hooks/useChannelPreviewPollLabel.js.map +1 -1
  3. package/lib/commonjs/components/Poll/Poll.js +5 -2
  4. package/lib/commonjs/components/Poll/Poll.js.map +1 -1
  5. package/lib/commonjs/components/Poll/components/PollAnswersList.js +3 -1
  6. package/lib/commonjs/components/Poll/components/PollAnswersList.js.map +1 -1
  7. package/lib/commonjs/components/Poll/components/PollOption.js +8 -3
  8. package/lib/commonjs/components/Poll/components/PollOption.js.map +1 -1
  9. package/lib/commonjs/components/Poll/components/PollResults/PollOptionFullResults.js +5 -2
  10. package/lib/commonjs/components/Poll/components/PollResults/PollOptionFullResults.js.map +1 -1
  11. package/lib/commonjs/components/Poll/components/PollResults/PollResultItem.js +3 -1
  12. package/lib/commonjs/components/Poll/components/PollResults/PollResultItem.js.map +1 -1
  13. package/lib/commonjs/components/Poll/components/PollResults/PollResults.js +4 -1
  14. package/lib/commonjs/components/Poll/components/PollResults/PollResults.js.map +1 -1
  15. package/lib/commonjs/components/Poll/hooks/usePollState.js +3 -0
  16. package/lib/commonjs/components/Poll/hooks/usePollState.js.map +1 -1
  17. package/lib/commonjs/components/Poll/hooks/usePollTextTranslation.js +17 -0
  18. package/lib/commonjs/components/Poll/hooks/usePollTextTranslation.js.map +1 -0
  19. package/lib/commonjs/components/Poll/index.js +11 -0
  20. package/lib/commonjs/components/Poll/index.js.map +1 -1
  21. package/lib/commonjs/hooks/messagePreview/useMessagePreviewText.js +9 -4
  22. package/lib/commonjs/hooks/messagePreview/useMessagePreviewText.js.map +1 -1
  23. package/lib/commonjs/hooks/useTranslatedMessage.js +4 -3
  24. package/lib/commonjs/hooks/useTranslatedMessage.js.map +1 -1
  25. package/lib/commonjs/store/SqliteClient.js +1 -1
  26. package/lib/commonjs/store/mappers/mapPollToStorable.js +4 -0
  27. package/lib/commonjs/store/mappers/mapPollToStorable.js.map +1 -1
  28. package/lib/commonjs/store/mappers/mapStorableToPoll.js +5 -0
  29. package/lib/commonjs/store/mappers/mapStorableToPoll.js.map +1 -1
  30. package/lib/commonjs/store/schema.js +2 -0
  31. package/lib/commonjs/store/schema.js.map +1 -1
  32. package/lib/commonjs/utils/i18n/getTranslatedText.js +7 -0
  33. package/lib/commonjs/utils/i18n/getTranslatedText.js.map +1 -0
  34. package/lib/commonjs/version.json +1 -1
  35. package/lib/module/components/ChannelPreview/hooks/useChannelPreviewPollLabel.js +8 -5
  36. package/lib/module/components/ChannelPreview/hooks/useChannelPreviewPollLabel.js.map +1 -1
  37. package/lib/module/components/Poll/Poll.js +5 -2
  38. package/lib/module/components/Poll/Poll.js.map +1 -1
  39. package/lib/module/components/Poll/components/PollAnswersList.js +3 -1
  40. package/lib/module/components/Poll/components/PollAnswersList.js.map +1 -1
  41. package/lib/module/components/Poll/components/PollOption.js +8 -3
  42. package/lib/module/components/Poll/components/PollOption.js.map +1 -1
  43. package/lib/module/components/Poll/components/PollResults/PollOptionFullResults.js +5 -2
  44. package/lib/module/components/Poll/components/PollResults/PollOptionFullResults.js.map +1 -1
  45. package/lib/module/components/Poll/components/PollResults/PollResultItem.js +3 -1
  46. package/lib/module/components/Poll/components/PollResults/PollResultItem.js.map +1 -1
  47. package/lib/module/components/Poll/components/PollResults/PollResults.js +4 -1
  48. package/lib/module/components/Poll/components/PollResults/PollResults.js.map +1 -1
  49. package/lib/module/components/Poll/hooks/usePollState.js +3 -0
  50. package/lib/module/components/Poll/hooks/usePollState.js.map +1 -1
  51. package/lib/module/components/Poll/hooks/usePollTextTranslation.js +17 -0
  52. package/lib/module/components/Poll/hooks/usePollTextTranslation.js.map +1 -0
  53. package/lib/module/components/Poll/index.js +11 -0
  54. package/lib/module/components/Poll/index.js.map +1 -1
  55. package/lib/module/hooks/messagePreview/useMessagePreviewText.js +9 -4
  56. package/lib/module/hooks/messagePreview/useMessagePreviewText.js.map +1 -1
  57. package/lib/module/hooks/useTranslatedMessage.js +4 -3
  58. package/lib/module/hooks/useTranslatedMessage.js.map +1 -1
  59. package/lib/module/store/SqliteClient.js +1 -1
  60. package/lib/module/store/mappers/mapPollToStorable.js +4 -0
  61. package/lib/module/store/mappers/mapPollToStorable.js.map +1 -1
  62. package/lib/module/store/mappers/mapStorableToPoll.js +5 -0
  63. package/lib/module/store/mappers/mapStorableToPoll.js.map +1 -1
  64. package/lib/module/store/schema.js +2 -0
  65. package/lib/module/store/schema.js.map +1 -1
  66. package/lib/module/utils/i18n/getTranslatedText.js +7 -0
  67. package/lib/module/utils/i18n/getTranslatedText.js.map +1 -0
  68. package/lib/module/version.json +1 -1
  69. package/lib/typescript/components/ChannelPreview/hooks/useChannelPreviewPollLabel.d.ts.map +1 -1
  70. package/lib/typescript/components/Poll/Poll.d.ts.map +1 -1
  71. package/lib/typescript/components/Poll/components/PollAnswersList.d.ts.map +1 -1
  72. package/lib/typescript/components/Poll/components/PollOption.d.ts.map +1 -1
  73. package/lib/typescript/components/Poll/components/PollResults/PollOptionFullResults.d.ts.map +1 -1
  74. package/lib/typescript/components/Poll/components/PollResults/PollResultItem.d.ts.map +1 -1
  75. package/lib/typescript/components/Poll/components/PollResults/PollResults.d.ts.map +1 -1
  76. package/lib/typescript/components/Poll/hooks/usePollState.d.ts +2 -1
  77. package/lib/typescript/components/Poll/hooks/usePollState.d.ts.map +1 -1
  78. package/lib/typescript/components/Poll/hooks/usePollTextTranslation.d.ts +8 -0
  79. package/lib/typescript/components/Poll/hooks/usePollTextTranslation.d.ts.map +1 -0
  80. package/lib/typescript/components/Poll/index.d.ts +1 -0
  81. package/lib/typescript/components/Poll/index.d.ts.map +1 -1
  82. package/lib/typescript/hooks/messagePreview/useMessagePreviewText.d.ts.map +1 -1
  83. package/lib/typescript/hooks/useTranslatedMessage.d.ts.map +1 -1
  84. package/lib/typescript/store/mappers/mapPollToStorable.d.ts.map +1 -1
  85. package/lib/typescript/store/mappers/mapStorableToPoll.d.ts.map +1 -1
  86. package/lib/typescript/store/schema.d.ts +2 -0
  87. package/lib/typescript/store/schema.d.ts.map +1 -1
  88. package/lib/typescript/utils/i18n/getTranslatedText.d.ts +6 -0
  89. package/lib/typescript/utils/i18n/getTranslatedText.d.ts.map +1 -0
  90. package/package.json +1 -1
  91. package/src/components/ChannelPreview/hooks/__tests__/useChannelPreviewPollLabel.test.tsx +31 -6
  92. package/src/components/ChannelPreview/hooks/useChannelPreviewPollLabel.ts +8 -4
  93. package/src/components/Poll/Poll.tsx +4 -2
  94. package/src/components/Poll/__tests__/PollTranslation.test.tsx +152 -0
  95. package/src/components/Poll/components/PollAnswersList.tsx +5 -1
  96. package/src/components/Poll/components/PollOption.tsx +8 -4
  97. package/src/components/Poll/components/PollResults/PollOptionFullResults.tsx +5 -2
  98. package/src/components/Poll/components/PollResults/PollResultItem.tsx +3 -1
  99. package/src/components/Poll/components/PollResults/PollResults.tsx +4 -2
  100. package/src/components/Poll/hooks/usePollState.ts +5 -0
  101. package/src/components/Poll/hooks/usePollTextTranslation.ts +21 -0
  102. package/src/components/Poll/index.ts +1 -0
  103. package/src/hooks/__tests__/useTranslatedMessage.test.tsx +20 -0
  104. package/src/hooks/messagePreview/__tests__/useMessagePreviewText.test.tsx +37 -2
  105. package/src/hooks/messagePreview/useMessagePreviewText.tsx +5 -2
  106. package/src/hooks/useTranslatedMessage.ts +7 -7
  107. package/src/store/SqliteClient.ts +1 -1
  108. package/src/store/apis/__tests__/pollTranslations.test.ts +136 -0
  109. package/src/store/mappers/mapPollToStorable.ts +6 -0
  110. package/src/store/mappers/mapStorableToPoll.ts +4 -0
  111. package/src/store/schema.ts +4 -0
  112. package/src/utils/i18n/__tests__/getTranslatedText.test.ts +19 -0
  113. package/src/utils/i18n/getTranslatedText.ts +9 -0
  114. package/src/version.json +1 -1
@@ -0,0 +1,152 @@
1
+ import React from 'react';
2
+
3
+ import { act, render, screen } from '@testing-library/react-native';
4
+ import dayjs from 'dayjs';
5
+ import { Poll, PollAnswer, PollResponse, StreamChat, TranslationLanguages } from 'stream-chat';
6
+
7
+ import {
8
+ ChatProvider,
9
+ OwnCapabilitiesProvider,
10
+ PollContextProvider,
11
+ ThemeProvider,
12
+ TranslationProvider,
13
+ } from '../../../contexts';
14
+ import { generateMessage } from '../../../mock-builders/generator/message';
15
+ import { PollAnswerListItem } from '../components/PollAnswersList';
16
+ import { PollAllOptionsContent, PollOption } from '../components/PollOption';
17
+ import { PollResultsContent } from '../components/PollResults/PollResults';
18
+ import { PollHeader } from '../Poll';
19
+
20
+ const POLL_ID = 'poll-id';
21
+
22
+ const pollResponse = {
23
+ created_by: { id: 'other-user' },
24
+ enforce_unique_vote: true,
25
+ id: POLL_ID,
26
+ latest_answers: [],
27
+ latest_votes_by_option: {},
28
+ name: 'Favourite colour?',
29
+ name_i18n: { es_text: '¿Color favorito?', language: 'en' },
30
+ options: [
31
+ { id: 'option-red', text: 'Red', text_i18n: { es_text: 'Rojo', language: 'en' } },
32
+ { id: 'option-blue', text: 'Blue' },
33
+ ],
34
+ vote_count: 0,
35
+ vote_counts_by_option: {},
36
+ } as unknown as PollResponse;
37
+
38
+ const answer = {
39
+ answer_text: 'Green, actually',
40
+ answer_text_i18n: { es_text: 'Verde, en realidad', language: 'en' },
41
+ created_at: '2026-10-27T10:00:00.000Z',
42
+ id: 'answer-id',
43
+ is_answer: true,
44
+ poll_id: POLL_ID,
45
+ updated_at: '2026-10-27T10:00:00.000Z',
46
+ user: { id: 'other-user', name: 'Other' },
47
+ } as unknown as PollAnswer;
48
+
49
+ const setup = (userLanguage: TranslationLanguages) => {
50
+ const client = { user: { id: 'own-user' }, userID: 'own-user' } as unknown as StreamChat;
51
+ const poll = new Poll({ client, poll: pollResponse });
52
+ const [red, blue] = poll.data.options;
53
+
54
+ render(
55
+ <ThemeProvider>
56
+ <ChatProvider value={{ client } as never}>
57
+ <TranslationProvider
58
+ value={{ t: (key: string) => key, tDateTimeParser: dayjs, userLanguage } as never}
59
+ >
60
+ <OwnCapabilitiesProvider value={{ castPollVote: true } as never}>
61
+ <PollContextProvider value={{ message: generateMessage(), poll }}>
62
+ <PollHeader />
63
+ <PollOption option={red} />
64
+ <PollOption option={blue} />
65
+ <PollAnswerListItem answer={answer} />
66
+ </PollContextProvider>
67
+ </OwnCapabilitiesProvider>
68
+ </TranslationProvider>
69
+ </ChatProvider>
70
+ </ThemeProvider>,
71
+ );
72
+
73
+ return { poll };
74
+ };
75
+
76
+ const renderDialogs = (userLanguage: TranslationLanguages) => {
77
+ const client = { user: { id: 'own-user' }, userID: 'own-user' } as unknown as StreamChat;
78
+ const poll = new Poll({ client, poll: pollResponse });
79
+
80
+ return render(
81
+ <ThemeProvider>
82
+ <ChatProvider value={{ client } as never}>
83
+ <TranslationProvider value={{ t: (key: string) => key, userLanguage } as never}>
84
+ <OwnCapabilitiesProvider value={{} as never}>
85
+ <PollContextProvider value={{ message: generateMessage(), poll }}>
86
+ <PollResultsContent />
87
+ <PollAllOptionsContent />
88
+ </PollContextProvider>
89
+ </OwnCapabilitiesProvider>
90
+ </TranslationProvider>
91
+ </ChatProvider>
92
+ </ThemeProvider>,
93
+ );
94
+ };
95
+
96
+ describe('Poll translations', () => {
97
+ it("renders the question, options and answers in the user's language", () => {
98
+ setup('es');
99
+
100
+ expect(screen.getByText('¿Color favorito?')).toBeTruthy();
101
+ expect(screen.getByText('Rojo')).toBeTruthy();
102
+ expect(screen.getByLabelText('Rojo')).toBeTruthy();
103
+ expect(screen.getByText('Verde, en realidad')).toBeTruthy();
104
+ expect(screen.queryByText('Favourite colour?')).toBeNull();
105
+ expect(screen.queryByText('Red')).toBeNull();
106
+ });
107
+
108
+ it('falls back to the original text when a text has no translation', () => {
109
+ setup('es');
110
+
111
+ expect(screen.getByText('Blue')).toBeTruthy();
112
+ expect(screen.getByLabelText('Blue')).toBeTruthy();
113
+ });
114
+
115
+ it("renders the original texts when nothing is translated into the user's language", () => {
116
+ setup('de');
117
+
118
+ expect(screen.getByText('Favourite colour?')).toBeTruthy();
119
+ expect(screen.getByText('Red')).toBeTruthy();
120
+ expect(screen.getByText('Blue')).toBeTruthy();
121
+ expect(screen.getByText('Green, actually')).toBeTruthy();
122
+ });
123
+
124
+ it('shows a translation that arrives later through poll.updated', () => {
125
+ const { poll } = setup('de');
126
+ expect(screen.getByText('Favourite colour?')).toBeTruthy();
127
+
128
+ act(() => {
129
+ poll.handlePollUpdated({
130
+ created_at: '2026-10-27T11:00:00.000Z',
131
+ poll: {
132
+ ...pollResponse,
133
+ name_i18n: { de_text: 'Lieblingsfarbe?', language: 'en' },
134
+ },
135
+ type: 'poll.updated',
136
+ } as never);
137
+ });
138
+
139
+ expect(screen.getByText('Lieblingsfarbe?')).toBeTruthy();
140
+ expect(screen.queryByText('Favourite colour?')).toBeNull();
141
+ });
142
+
143
+ it('translates the question and options in the results and all-options dialogs', () => {
144
+ renderDialogs('es');
145
+
146
+ // Each dialog shows the question once and every option once.
147
+ expect(screen.getAllByText('¿Color favorito?')).toHaveLength(2);
148
+ expect(screen.getAllByText('Rojo')).toHaveLength(2);
149
+ expect(screen.getAllByText('Blue')).toHaveLength(2);
150
+ expect(screen.queryByText('Favourite colour?')).toBeNull();
151
+ });
152
+ });
@@ -21,6 +21,7 @@ import { Button } from '../../ui';
21
21
  import { UserAvatar } from '../../ui/Avatar/UserAvatar';
22
22
  import { usePollAnswersPagination } from '../hooks/usePollAnswersPagination';
23
23
  import { usePollState } from '../hooks/usePollState';
24
+ import { usePollTextTranslation } from '../hooks/usePollTextTranslation';
24
25
 
25
26
  export const AnswerListAddCommentButton = (props: PollButtonProps) => {
26
27
  const { t } = useTranslationContext();
@@ -71,6 +72,7 @@ export const PollAnswerListItem = ({ answer }: { answer: PollAnswer }) => {
71
72
  const { client } = useChatContext();
72
73
  const { t, tDateTimeParser } = useTranslationContext();
73
74
  const { votingVisibility } = usePollState();
75
+ const translate = usePollTextTranslation();
74
76
 
75
77
  const {
76
78
  theme: {
@@ -104,7 +106,9 @@ export const PollAnswerListItem = ({ answer }: { answer: PollAnswer }) => {
104
106
  return (
105
107
  <View style={[styles.listItemWrapper, itemStyle.wrapper]}>
106
108
  <View style={[styles.listItemContainer, itemStyle.container]}>
107
- <Text style={[styles.listItemAnswerText, itemStyle.answerText]}>{answer.answer_text}</Text>
109
+ <Text style={[styles.listItemAnswerText, itemStyle.answerText]}>
110
+ {translate(answer.answer_text, answer.answer_text_i18n)}
111
+ </Text>
108
112
  <View style={[styles.listItemInfoContainer, itemStyle.infoContainer]}>
109
113
  <View style={[styles.listItemUserInfoContainer, itemStyle.userInfoContainer]}>
110
114
  {!isAnonymous && answer.user?.image ? (
@@ -23,6 +23,7 @@ import { ProgressBar } from '../../ProgressControl/ProgressBar';
23
23
  import { UserAvatarStack } from '../../ui/Avatar/AvatarStack';
24
24
  import { useIsPollCreatedByCurrentUser } from '../hook/useIsPollCreatedByCurrentUser';
25
25
  import { usePollState } from '../hooks/usePollState';
26
+ import { usePollTextTranslation } from '../hooks/usePollTextTranslation';
26
27
  import { usePollVoteToggle } from '../hooks/usePollVoteToggle';
27
28
 
28
29
  const pollVoteAccessibilityStates = {
@@ -44,7 +45,8 @@ export const PollAllOptionsContent = ({
44
45
  additionalScrollViewProps,
45
46
  }: Pick<PollAllOptionsContentProps, 'additionalScrollViewProps'>) => {
46
47
  const { t } = useTranslationContext();
47
- const { name, options } = usePollState();
48
+ const { name, nameI18n, options } = usePollState();
49
+ const translate = usePollTextTranslation();
48
50
 
49
51
  const {
50
52
  theme: {
@@ -63,7 +65,7 @@ export const PollAllOptionsContent = ({
63
65
  >
64
66
  <View style={[styles.allOptionsTitleContainer, titleContainer]}>
65
67
  <Text style={styles.allOptionsTitleMeta}>{t('Question')}</Text>
66
- <Text style={[styles.allOptionsTitleText, titleText]}>{name}</Text>
68
+ <Text style={[styles.allOptionsTitleText, titleText]}>{translate(name, nameI18n)}</Text>
67
69
  </View>
68
70
  <View style={[styles.allOptionsListContainer, listContainer]}>
69
71
  {options?.map((option: PollOptionClass) => (
@@ -92,6 +94,7 @@ export const PollAllOptions = ({
92
94
  export const PollOption = ({ option, showProgressBar = true, forceIncoming }: PollOptionProps) => {
93
95
  const { latestVotesByOption, voteCountsByOption, voteCount } = usePollState();
94
96
  const { t } = useTranslationContext();
97
+ const translate = usePollTextTranslation();
95
98
  const isPollCreatedByClient = useIsPollCreatedByCurrentUser();
96
99
  // `forceIncoming` is set on the full-options surface, which is a neutral card
97
100
  // rather than a bubble - so it wins over poll ownership.
@@ -129,7 +132,7 @@ export const PollOption = ({ option, showProgressBar = true, forceIncoming }: Po
129
132
  <View style={[styles.info, info]}>
130
133
  <View style={[styles.header, header]}>
131
134
  <Text ellipsizeMode='tail' numberOfLines={2} style={[styles.text, text]}>
132
- {option.text}
135
+ {translate(option.text, option.text_i18n)}
133
136
  </Text>
134
137
  <View style={[styles.votesContainer, votesContainer]}>
135
138
  {relevantVotes.length > 0 ? (
@@ -186,6 +189,7 @@ export const VoteButton = ({ forceIncoming, onPress, option }: PollVoteButtonPro
186
189
  } = useTheme();
187
190
 
188
191
  const toggleVote = usePollVoteToggle();
192
+ const translate = usePollTextTranslation();
189
193
 
190
194
  const onPressHandler = useCallback(() => {
191
195
  if (onPress) {
@@ -203,7 +207,7 @@ export const VoteButton = ({ forceIncoming, onPress, option }: PollVoteButtonPro
203
207
 
204
208
  return ownCapabilities.castPollVote && !isClosed ? (
205
209
  <Pressable
206
- accessibilityLabel={option.text}
210
+ accessibilityLabel={translate(option.text, option.text_i18n)}
207
211
  accessibilityRole={poll.data?.enforce_unique_vote ? 'radio' : 'checkbox'}
208
212
  accessibilityState={accessibilityState}
209
213
  onPress={onPressHandler}
@@ -16,6 +16,7 @@ import { useComponentsContext } from '../../../../contexts/componentsContext/Com
16
16
  import { primitives } from '../../../../theme';
17
17
  import { usePollOptionVotesPagination } from '../../hooks/usePollOptionVotesPagination';
18
18
  import { usePollState } from '../../hooks/usePollState';
19
+ import { usePollTextTranslation } from '../../hooks/usePollTextTranslation';
19
20
 
20
21
  export type PollOptionFullResultsProps = PollContextValue & {
21
22
  option: PollOption;
@@ -33,6 +34,8 @@ export const PollOptionFullResultsContent = ({
33
34
  const { t } = useTranslationContext();
34
35
  const { hasNextPage, loadMore, votes } = usePollOptionVotesPagination({ option });
35
36
  const { voteCountsByOption } = usePollState();
37
+ const translate = usePollTextTranslation();
38
+ const optionText = translate(option.text, option.text_i18n);
36
39
 
37
40
  const {
38
41
  theme: {
@@ -46,7 +49,7 @@ export const PollOptionFullResultsContent = ({
46
49
  const PollOptionFullResultsHeader = useCallback(
47
50
  () => (
48
51
  <View style={[styles.headerContainer, headerContainer]}>
49
- <Text style={[styles.headerTitle, headerTitle]}>{option.text}</Text>
52
+ <Text style={[styles.headerTitle, headerTitle]}>{optionText}</Text>
50
53
  <Text style={[styles.headerText, headerText]}>
51
54
  {t('{{count}} votes', { count: voteCountsByOption[option.id] ?? 0 })}
52
55
  </Text>
@@ -57,7 +60,7 @@ export const PollOptionFullResultsContent = ({
57
60
  headerText,
58
61
  headerTitle,
59
62
  option.id,
60
- option.text,
63
+ optionText,
61
64
  styles.headerContainer,
62
65
  styles.headerText,
63
66
  styles.headerTitle,
@@ -15,6 +15,7 @@ import {
15
15
 
16
16
  import { primitives } from '../../../../theme';
17
17
  import { usePollState } from '../../hooks/usePollState';
18
+ import { usePollTextTranslation } from '../../hooks/usePollTextTranslation';
18
19
  import { GenericPollButton } from '../Button';
19
20
  import { PollModal } from '../PollModal';
20
21
  import { PollModalHeader } from '../PollModalHeader';
@@ -86,6 +87,7 @@ const PollResultsVoteItem = (vote: PollVoteClass) => (
86
87
  export const PollResultsItem = ({ option, index }: PollResultItemProps) => {
87
88
  const { t } = useTranslationContext();
88
89
  const { latestVotesByOption, voteCountsByOption } = usePollState();
90
+ const translate = usePollTextTranslation();
89
91
 
90
92
  const {
91
93
  theme: {
@@ -105,7 +107,7 @@ export const PollResultsItem = ({ option, index }: PollResultItemProps) => {
105
107
  {t('Option {{count}}', { count: index + 1 })}
106
108
  </Text>
107
109
  <View style={[styles.headerContainer, headerContainer]}>
108
- <Text style={[styles.title, title]}>{option.text}</Text>
110
+ <Text style={[styles.title, title]}>{translate(option.text, option.text_i18n)}</Text>
109
111
  <Text style={[styles.voteCount, voteCount]}>
110
112
  {t('{{count}} votes', { count: voteCountsByOption[option.id] ?? 0 })}
111
113
  </Text>
@@ -14,6 +14,7 @@ import {
14
14
  import { useComponentsContext } from '../../../../contexts/componentsContext/ComponentsContext';
15
15
  import { primitives } from '../../../../theme';
16
16
  import { usePollState } from '../../hooks/usePollState';
17
+ import { usePollTextTranslation } from '../../hooks/usePollTextTranslation';
17
18
 
18
19
  export type PollResultsProps = PollContextValue & {
19
20
  additionalScrollViewProps?: Partial<ScrollViewProps>;
@@ -22,7 +23,8 @@ export type PollResultsProps = PollContextValue & {
22
23
  export const PollResultsContent = ({
23
24
  additionalScrollViewProps,
24
25
  }: Pick<PollResultsProps, 'additionalScrollViewProps'>) => {
25
- const { name, options, voteCountsByOption } = usePollState();
26
+ const { name, nameI18n, options, voteCountsByOption } = usePollState();
27
+ const translate = usePollTextTranslation();
26
28
 
27
29
  const sortedOptions = useMemo(
28
30
  () =>
@@ -51,7 +53,7 @@ export const PollResultsContent = ({
51
53
  >
52
54
  <View style={[styles.container, container]}>
53
55
  <Text style={[styles.titleMeta, titleMeta]}>{t('Question')}</Text>
54
- <Text style={[styles.title, title]}>{name}</Text>
56
+ <Text style={[styles.title, title]}>{translate(name, nameI18n)}</Text>
55
57
  </View>
56
58
  <View style={styles.resultsContainer}>
57
59
  {sortedOptions.map((option, index) => (
@@ -7,6 +7,7 @@ import {
7
7
  PollOption,
8
8
  PollState,
9
9
  PollVote,
10
+ TranslatedField,
10
11
  UpdatePollAPIResponse,
11
12
  UserResponse,
12
13
  VotingVisibility,
@@ -28,6 +29,7 @@ export type UsePollStateSelectorReturnType = {
28
29
  maxVotedOptionIds: string[];
29
30
  maxVotesAllowed: number;
30
31
  name: string;
32
+ nameI18n: TranslatedField | undefined;
31
33
  options: PollOption[];
32
34
  ownAnswer: PollAnswer | undefined;
33
35
  ownVotesByOptionId: Record<string, PollVote>;
@@ -53,6 +55,7 @@ const selector = (nextValue: PollState): UsePollStateSelectorReturnType => ({
53
55
  maxVotedOptionIds: nextValue.maxVotedOptionIds,
54
56
  maxVotesAllowed: nextValue.max_votes_allowed,
55
57
  name: nextValue.name,
58
+ nameI18n: nextValue.name_i18n,
56
59
  options: nextValue.options,
57
60
  ownAnswer: nextValue.ownAnswer,
58
61
  ownVotesByOptionId: nextValue.ownVotesByOptionId,
@@ -74,6 +77,7 @@ export const usePollState = (): UsePollStateReturnType => {
74
77
  maxVotedOptionIds,
75
78
  maxVotesAllowed,
76
79
  name,
80
+ nameI18n,
77
81
  options,
78
82
  ownAnswer,
79
83
  ownVotesByOptionId,
@@ -108,6 +112,7 @@ export const usePollState = (): UsePollStateReturnType => {
108
112
  maxVotedOptionIds,
109
113
  maxVotesAllowed,
110
114
  name,
115
+ nameI18n,
111
116
  options,
112
117
  ownAnswer,
113
118
  ownVotesByOptionId,
@@ -0,0 +1,21 @@
1
+ import { useCallback } from 'react';
2
+
3
+ import type { TranslatedField } from 'stream-chat';
4
+
5
+ import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
6
+ import { getTranslatedText } from '../../../utils/i18n/getTranslatedText';
7
+
8
+ /**
9
+ * Returns `translate(original, i18n)`, which resolves a poll text (question, option or answer)
10
+ * into the current user's language. Falls back to `original` when there is no translation for it,
11
+ * the same way auto-translated message text does.
12
+ */
13
+ export const usePollTextTranslation = () => {
14
+ const { userLanguage } = useTranslationContext();
15
+
16
+ return useCallback(
17
+ (original: string, i18n: TranslatedField | undefined): string =>
18
+ getTranslatedText(i18n, userLanguage) ?? original,
19
+ [userLanguage],
20
+ );
21
+ };
@@ -8,3 +8,4 @@ export * from './hooks/usePollState';
8
8
  export * from './hooks/usePollStateStore';
9
9
  export * from './hooks/usePollAnswersPagination';
10
10
  export * from './hooks/usePollOptionVotesPagination';
11
+ export * from './hooks/usePollTextTranslation';
@@ -59,6 +59,26 @@ describe('useTranslatedMessage', () => {
59
59
  });
60
60
  });
61
61
 
62
+ it("doesn't translate a deleted message", async () => {
63
+ const message = {
64
+ i18n: {
65
+ no_text: 'Hallo verden!',
66
+ },
67
+ text: 'Hello world!',
68
+ type: 'deleted',
69
+ } as unknown as MessageResponse;
70
+
71
+ render(
72
+ <TranslationProvider value={{ userLanguage: 'no' } as TranslationContextValue}>
73
+ <TestComponent message={message} />
74
+ </TranslationProvider>,
75
+ );
76
+
77
+ await waitFor(() => {
78
+ expect(screen.getByText('Hello world!')).toBeTruthy();
79
+ });
80
+ });
81
+
62
82
  it("returns the original text if the message doesn't contain any translations", async () => {
63
83
  const message = {
64
84
  text: 'Hello world!',
@@ -2,7 +2,7 @@ import React, { PropsWithChildren } from 'react';
2
2
 
3
3
  import { renderHook, waitFor } from '@testing-library/react-native';
4
4
 
5
- import type { Attachment } from 'stream-chat';
5
+ import type { Attachment, PollResponse, StreamChat } from 'stream-chat';
6
6
 
7
7
  import { Chat } from '../../../components/Chat/Chat';
8
8
  import { initiateClientWithChannels } from '../../../mock-builders/api/initiateClientWithChannels';
@@ -15,8 +15,12 @@ import { generateMessage } from '../../../mock-builders/generator/message';
15
15
  import { useMessagePreviewText } from '../useMessagePreviewText';
16
16
 
17
17
  describe('useMessagePreviewText', () => {
18
- const render = async (message: ReturnType<typeof generateMessage>) => {
18
+ const render = async (
19
+ message: ReturnType<typeof generateMessage>,
20
+ prepareClient?: (client: StreamChat) => void,
21
+ ) => {
19
22
  const { client } = await initiateClientWithChannels();
23
+ prepareClient?.(client);
20
24
  const wrapper = ({ children }: PropsWithChildren) => <Chat client={client}>{children}</Chat>;
21
25
  const { result } = renderHook(() => useMessagePreviewText({ message }), { wrapper });
22
26
  return result;
@@ -56,4 +60,35 @@ describe('useMessagePreviewText', () => {
56
60
  );
57
61
  expect(result.current).toBe('hello');
58
62
  });
63
+
64
+ describe('poll messages', () => {
65
+ const poll = {
66
+ id: 'poll-id',
67
+ latest_answers: [],
68
+ latest_votes_by_option: {},
69
+ name: 'Favourite colour?',
70
+ name_i18n: { es_text: '¿Color favorito?', language: 'en' },
71
+ options: [],
72
+ vote_count: 0,
73
+ vote_counts_by_option: {},
74
+ } as unknown as PollResponse;
75
+
76
+ const renderPollMessage = (language: string) => {
77
+ const message = generateMessage({ poll, poll_id: poll.id, text: '' });
78
+ return render(message, (client) => {
79
+ (client.user as { language?: string }).language = language;
80
+ client.polls.hydratePollCache([message]);
81
+ });
82
+ };
83
+
84
+ it("shows the poll question in the user's language", async () => {
85
+ const result = await renderPollMessage('es');
86
+ expect(result.current).toBe('¿Color favorito?');
87
+ });
88
+
89
+ it('shows the original poll question when it has no translation', async () => {
90
+ const result = await renderPollMessage('de');
91
+ expect(result.current).toBe('Favourite colour?');
92
+ });
93
+ });
59
94
  });
@@ -12,9 +12,11 @@ import { useGroupedAttachments } from './useGroupedAttachments';
12
12
  import { useChatContext } from '../../contexts/chatContext/ChatContext';
13
13
  import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
14
14
  import { useStateStore } from '../../hooks/useStateStore';
15
+ import { getTranslatedText } from '../../utils/i18n/getTranslatedText';
15
16
 
16
17
  const selector = (nextValue: PollState) => ({
17
18
  name: nextValue.name,
19
+ name_i18n: nextValue.name_i18n,
18
20
  });
19
21
 
20
22
  export const useMessagePreviewText = ({
@@ -22,10 +24,11 @@ export const useMessagePreviewText = ({
22
24
  }: {
23
25
  message?: LocalMessage | MessageResponse | DraftMessage | null;
24
26
  }) => {
25
- const { t } = useTranslationContext();
27
+ const { t, userLanguage } = useTranslationContext();
26
28
  const { client } = useChatContext();
27
29
  const poll = client.polls.fromState(message?.poll_id ?? '');
28
- const { name: pollName } = useStateStore(poll?.state, selector) ?? {};
30
+ const { name, name_i18n } = useStateStore(poll?.state, selector) ?? {};
31
+ const pollName = getTranslatedText(name_i18n, userLanguage) ?? name;
29
32
  const { giphys, audios, images, videos, files, voiceRecordings } = useGroupedAttachments(
30
33
  message?.attachments,
31
34
  );
@@ -1,22 +1,22 @@
1
- import type { LocalMessage, MessageResponse, TranslationLanguages } from 'stream-chat';
1
+ import type { LocalMessage, MessageResponse } from 'stream-chat';
2
2
 
3
3
  import { useTranslationContext } from '../contexts/translationContext/TranslationContext';
4
-
5
- type TranslationKey = `${TranslationLanguages}_text`;
4
+ import { getTranslatedText } from '../utils/i18n/getTranslatedText';
6
5
 
7
6
  export const useTranslatedMessage = (message?: LocalMessage | MessageResponse) => {
8
7
  const { userLanguage } = useTranslationContext();
9
8
 
10
- const translationKey: TranslationKey = `${userLanguage}_text`;
11
-
12
9
  if (!message) {
13
10
  return undefined;
14
11
  }
15
12
 
16
- if (message.i18n && translationKey in message.i18n && message.type !== 'deleted') {
13
+ const translatedText =
14
+ message.type !== 'deleted' ? getTranslatedText(message.i18n, userLanguage) : undefined;
15
+
16
+ if (translatedText !== undefined) {
17
17
  return {
18
18
  ...message,
19
- text: message.i18n[translationKey],
19
+ text: translatedText,
20
20
  };
21
21
  }
22
22
 
@@ -51,7 +51,7 @@ export class SqliteClientError extends Error {
51
51
  * This way usage @op-engineering/op-sqlite package is scoped to a single class/file.
52
52
  */
53
53
  export class SqliteClient {
54
- static dbVersion = 16;
54
+ static dbVersion = 17;
55
55
 
56
56
  static dbName = DB_NAME;
57
57
  static dbLocation = DB_LOCATION;