Skip to content

Commit 7867677

Browse files
authored
feat: make aria-labels localizable (#2282)
### 🎯 Goal We had several requests related to customization of `aria-label` attributes. The best approach is to rely on existing internationalization support for customization and overrides. Fixes #1931, fixes #1994. ### 🛠 Implementation details 1. Wrapped all ARIA labels in `t(...)` translation function. The keys used for ARIA labels are prefixed with `aria/...` to distinguish them from other texts. 2. Added default translations for supported languages. ### 🎨 UI Changes No visible changes. ### To-Do - [x] Update docs
1 parent 8f48b52 commit 7867677

40 files changed

+434
-98
lines changed

docusaurus/docs/React/guides/theming/translations.mdx

+45-11
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,38 @@ JSON objects can be imported from the library.
132132
`import { esTranslations } from 'stream-chat-react';`
133133
:::
134134

135+
### Overriding ARIA labels
136+
137+
ARIA labels that are used for interactive elements are also subject to localization. Translation keys for ARIA labels are prefixed by `aria/`:
138+
139+
```jsx
140+
import { useTranslationContext } from 'stream-chat-react';
141+
142+
const Component = () => {
143+
const { t } = useTranslationContext();
144+
return (
145+
<button type='button' aria-label={t('aria/Send')}>
146+
📨
147+
</button>
148+
);
149+
};
150+
```
151+
152+
To override the default translations, add an `aria`-prefixed key to the `translationsForLanguage` object:
153+
154+
```jsx
155+
const i18nInstance = new Streami18n({
156+
language: 'en',
157+
translationsForLanguage: {
158+
'aria/Send': 'Send Message',
159+
},
160+
});
161+
162+
<Chat client={client} i18nInstance={i18nInstance}>
163+
{/* ... */}
164+
</Chat>;
165+
```
166+
135167
## Add a Language
136168

137169
In the following example, we will demonstrate how to add translation support for an additional language not currently supported
@@ -293,7 +325,7 @@ To display date and time in different than machine's local timezone, provide the
293325
```ts
294326
import { Streami18n } from 'stream-chat-react';
295327

296-
const streamI18n = new Streami18n({ timezone: 'Europe/Prague'});
328+
const streamI18n = new Streami18n({ timezone: 'Europe/Prague' });
297329
```
298330
299331
If you are using `moment` as your datetime parser engine and want to start using timezone-located datetime strings, then we recommend to use `moment-timezone` instead of `moment` package. Moment Timezone will automatically load and extend the moment module, then return the modified instance. This will also prevent multiple versions of `moment` being installed in a project.
@@ -305,7 +337,7 @@ import { Streami18n } from 'stream-chat-react';
305337
const i18n = new Streami18n({
306338
DateTimeParser: momentTimezone,
307339
timezone: 'Europe/Prague',
308-
})
340+
});
309341
```
310342
311343
### Translating Messages
@@ -331,7 +363,7 @@ The `Streami18n` class wraps [`i18next`](https://www.npmjs.com/package/i18next)
331363
### Class Constructor Options
332364
333365
| Option | Description | Type | Default |
334-
|------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------------------------|
366+
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------- |
335367
| DateTimeParser | custom date time parser | function | Day.js |
336368
| dayjsLocaleConfigForLanguage | internal Day.js [config object](https://github.com/iamkun/dayjs/tree/dev/src/locale) and [calendar locale config object](https://day.js.org/docs/en/plugin/calendar) | object | 'enConfig' |
337369
| debug | enables i18n debug mode | boolean | false |
@@ -350,21 +382,23 @@ The default implementation returns the default value provided to the translator
350382
import { useTranslationContext } from 'stream-chat-react';
351383

352384
const Component = () => {
353-
const { t } = useTranslationContext('useCommandTrigger');
385+
const { t } = useTranslationContext('useCommandTrigger');
354386

355-
return (
356-
<div>{t('some-key', {defaultValue: 'hello'})}</div>
357-
);
358-
}
387+
return <div>{t('some-key', { defaultValue: 'hello' })}</div>;
388+
};
359389
```
390+
360391
The custom handler may log missing key warnings to the console in the development environment:
361392
362393
```ts
363394
import { Streami18n, Streami18nOptions } from 'stream-chat-react';
364395

365-
const parseMissingKeyHandler: Streami18nOptions['parseMissingKeyHandler'] = (key: string, defaultValue?: string) => {
366-
console.warn(`Streami18n: Missing translation for key: ${key}`);
367-
return defaultValue ?? key;
396+
const parseMissingKeyHandler: Streami18nOptions['parseMissingKeyHandler'] = (
397+
key: string,
398+
defaultValue?: string,
399+
) => {
400+
console.warn(`Streami18n: Missing translation for key: ${key}`);
401+
return defaultValue ?? key;
368402
};
369403

370404
const i18nInstance = new Streami18n({ parseMissingKeyHandler });

src/components/Attachment/__tests__/Card.test.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import {
1515
generateUser,
1616
getOrCreateChannelApi,
1717
getTestClientWithUser,
18+
mockTranslationContext,
1819
useMockedApis,
1920
} from '../../../mock-builders';
21+
import { TranslationContext } from '../../../context';
2022

2123
let chatClient;
2224
let channel;
@@ -31,11 +33,13 @@ const mockedChannel = generateChannel({
3133
const renderCard = ({ cardProps, chatContext, theRenderer = render }) =>
3234
theRenderer(
3335
<ChatProvider value={{ themeVersion: '1', ...chatContext }}>
34-
<ChannelStateProvider value={{}}>
35-
<ComponentProvider value={{}}>
36-
<Card {...cardProps} />
37-
</ComponentProvider>
38-
</ChannelStateProvider>
36+
<TranslationContext.Provider value={mockTranslationContext}>
37+
<ChannelStateProvider value={{}}>
38+
<ComponentProvider value={{}}>
39+
<Card {...cardProps} />
40+
</ComponentProvider>
41+
</ChannelStateProvider>
42+
</TranslationContext.Provider>
3943
</ChatProvider>,
4044
);
4145

src/components/Attachment/__tests__/File.test.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ import renderer from 'react-test-renderer';
55
import { FileAttachment } from '../FileAttachment';
66

77
import { ChatContext } from '../../../context/ChatContext';
8+
import { TranslationContext } from '../../../context';
9+
import { mockTranslationContext } from '../../../mock-builders';
810

911
const getComponent = ({ attachment, chatContext }) => (
1012
<ChatContext.Provider value={chatContext}>
11-
<FileAttachment attachment={attachment} />
13+
<TranslationContext.Provider value={mockTranslationContext}>
14+
<FileAttachment attachment={attachment} />
15+
</TranslationContext.Provider>
1216
</ChatContext.Provider>
1317
);
1418

src/components/ChannelHeader/ChannelHeader.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ const UnMemoizedChannelHeader = <
5050

5151
return (
5252
<div className='str-chat__header-livestream str-chat__channel-header'>
53-
<button aria-label='Menu' className='str-chat__header-hamburger' onClick={openMobileNav}>
53+
<button
54+
aria-label={t('aria/Menu')}
55+
className='str-chat__header-hamburger'
56+
onClick={openMobileNav}
57+
>
5458
<MenuIcon />
5559
</button>
5660
<Avatar

src/components/ChannelList/ChannelListMessenger.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { LoadingChannels } from '../Loading/LoadingChannels';
66
import type { APIErrorResponse, Channel, ErrorFromResponse } from 'stream-chat';
77

88
import type { DefaultStreamChatGenerics } from '../../types/types';
9+
import { useTranslationContext } from '../../context';
910

1011
export type ChannelListMessengerProps<
1112
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
@@ -39,6 +40,7 @@ export const ChannelListMessenger = <
3940
LoadingErrorIndicator = ChatDown,
4041
LoadingIndicator = LoadingChannels,
4142
} = props;
43+
const { t } = useTranslationContext('ChannelListMessenger');
4244

4345
if (error) {
4446
return <LoadingErrorIndicator type='Connection Error' />;
@@ -51,7 +53,7 @@ export const ChannelListMessenger = <
5153
return (
5254
<div className='str-chat__channel-list-messenger str-chat__channel-list-messenger-react'>
5355
<div
54-
aria-label='Channel list'
56+
aria-label={t('aria/Channel list')}
5557
className='str-chat__channel-list-messenger__main str-chat__channel-list-messenger-react__main'
5658
role='listbox'
5759
>

src/components/ChannelList/__tests__/ChannelList.test.js

+15-8
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,14 @@ import {
3636
ChannelPreviewMessenger,
3737
} from '../../ChannelPreview';
3838

39-
import { ChatContext, useChannelListContext, useChatContext } from '../../../context';
39+
import {
40+
ChatContext,
41+
TranslationContext,
42+
useChannelListContext,
43+
useChatContext,
44+
} from '../../../context';
4045
import { ChannelListMessenger } from '../ChannelListMessenger';
41-
import { initClientWithChannels } from '../../../mock-builders';
46+
import { initClientWithChannels, mockTranslationContext } from '../../../mock-builders';
4247

4348
expect.extend(toHaveNoViolations);
4449

@@ -603,12 +608,14 @@ describe('ChannelList', () => {
603608
...chatContext,
604609
}}
605610
>
606-
<ChannelList
607-
filters={{}}
608-
options={{ presence: true, state: true }}
609-
showChannelSearch
610-
{...channeListProps}
611-
/>
611+
<TranslationContext.Provider value={mockTranslationContext}>
612+
<ChannelList
613+
filters={{}}
614+
options={{ presence: true, state: true }}
615+
showChannelSearch
616+
{...channeListProps}
617+
/>
618+
</TranslationContext.Provider>
612619
</ChatContext.Provider>,
613620
);
614621

src/components/ChannelList/__tests__/ChannelListMessenger.test.js

+13-9
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,25 @@ import '@testing-library/jest-dom';
44
import renderer from 'react-test-renderer';
55

66
import { ChannelListMessenger } from '../ChannelListMessenger';
7+
import { TranslationProvider } from '../../../context';
8+
import { mockTranslationContext } from '../../../mock-builders';
79

810
// Weird hack to avoid big warnings
911
// Maybe better to find a better solution for it.
1012
console.warn = () => null;
1113

1214
const Component = ({ error = false, loading = false }) => (
13-
<ChannelListMessenger
14-
error={error}
15-
loading={loading}
16-
LoadingErrorIndicator={() => <div>Loading Error Indicator</div>}
17-
LoadingIndicator={() => <div>Loading Indicator</div>}
18-
>
19-
<div>children 1</div>
20-
<div>children 2</div>
21-
</ChannelListMessenger>
15+
<TranslationProvider value={mockTranslationContext}>
16+
<ChannelListMessenger
17+
error={error}
18+
loading={loading}
19+
LoadingErrorIndicator={() => <div>Loading Error Indicator</div>}
20+
LoadingIndicator={() => <div>Loading Indicator</div>}
21+
>
22+
<div>children 1</div>
23+
<div>children 2</div>
24+
</ChannelListMessenger>
25+
</TranslationProvider>
2226
);
2327

2428
describe('ChannelListMessenger', () => {

src/components/ChannelSearch/SearchResults.tsx

+15-11
Original file line numberDiff line numberDiff line change
@@ -142,17 +142,21 @@ const DefaultSearchResultItem = <
142142
const ResultsContainer = ({
143143
children,
144144
popupResults,
145-
}: PropsWithChildren<{ popupResults?: boolean }>) => (
146-
<div
147-
aria-label='Channel search results'
148-
className={clsx(
149-
`str-chat__channel-search-container str-chat__channel-search-result-list`,
150-
popupResults ? 'popup' : 'inline',
151-
)}
152-
>
153-
{children}
154-
</div>
155-
);
145+
}: PropsWithChildren<{ popupResults?: boolean }>) => {
146+
const { t } = useTranslationContext('ResultsContainer');
147+
148+
return (
149+
<div
150+
aria-label={t('aria/Channel search results')}
151+
className={clsx(
152+
`str-chat__channel-search-container str-chat__channel-search-result-list`,
153+
popupResults ? 'popup' : 'inline',
154+
)}
155+
>
156+
{children}
157+
</div>
158+
);
159+
};
156160

157161
export type SearchResultsController<
158162
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics

src/components/Emojis/EmojiPicker.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export const EmojiPicker = (props: EmojiPickerProps) => {
101101
)}
102102
<button
103103
aria-expanded={displayPicker}
104-
aria-label='Emoji picker'
104+
aria-label={t('aria/Emoji picker')}
105105
className={props.buttonClassName ?? buttonClassName}
106106
onClick={() => setDisplayPicker((cv) => !cv)}
107107
ref={setReferenceElement}

src/components/Gallery/__tests__/BaseImage.test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ import '@testing-library/jest-dom';
44

55
import { BaseImage } from '../BaseImage';
66
import { TranslationProvider } from '../../../context';
7+
import { mockTranslationContext } from '../../../mock-builders';
78

89
const props = {
910
alt: 'alt',
1011
src: 'src',
1112
};
12-
const t = (val) => val;
1313
const BASE_IMAGE_TEST_ID = 'str-chat__base-image';
1414
const getImage = () => screen.queryByTestId(BASE_IMAGE_TEST_ID);
1515

1616
const renderComponent = (props = {}) =>
1717
render(
18-
<TranslationProvider value={{ t }}>
18+
<TranslationProvider value={mockTranslationContext}>
1919
<BaseImage {...props} />
2020
</TranslationProvider>,
2121
);
@@ -92,7 +92,7 @@ describe('BaseImage', () => {
9292
fireEvent.error(getImage());
9393

9494
rerender(
95-
<TranslationProvider value={{ t }}>
95+
<TranslationProvider value={mockTranslationContext}>
9696
<BaseImage src={'new-src'} />
9797
</TranslationProvider>,
9898
);

src/components/LoadMore/LoadMoreButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const UnMemoizedLoadMoreButton = ({
3434
return (
3535
<div className='str-chat__load-more-button'>
3636
<button
37-
aria-label='Load More Channels'
37+
aria-label={t('aria/Load More Channels')}
3838
className='str-chat__load-more-button__button str-chat__cta-button'
3939
data-testid='load-more-button'
4040
disabled={loading}

src/components/LoadMore/__tests__/LoadMoreButton.test.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@ import renderer from 'react-test-renderer';
44
import '@testing-library/jest-dom';
55

66
import { LoadMoreButton } from '../LoadMoreButton';
7+
import { TranslationProvider } from '../../../context';
8+
import { mockTranslationContext } from '../../../mock-builders';
79

810
describe('LoadMoreButton', () => {
911
afterEach(cleanup);
1012

1113
it('should render component with default props', () => {
1214
const tree = renderer
13-
.create(<LoadMoreButton isLoading={false} onClick={() => null} />)
15+
.create(
16+
<TranslationProvider value={mockTranslationContext}>
17+
<LoadMoreButton isLoading={false} onClick={() => null} />
18+
</TranslationProvider>,
19+
)
1420
.toJSON();
1521
expect(tree).toMatchInlineSnapshot(`
1622
<div

src/components/Message/MessageOptions.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { MessageActions } from '../MessageActions';
1212
import { MessageContextValue, useMessageContext } from '../../context/MessageContext';
1313

1414
import type { DefaultStreamChatGenerics, IconProps } from '../../types/types';
15+
import { useTranslationContext } from '../../context';
1516

1617
export type MessageOptionsProps<
1718
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
@@ -56,6 +57,8 @@ const UnMemoizedMessageOptions = <
5657
threadList,
5758
} = useMessageContext<StreamChatGenerics>('MessageOptions');
5859

60+
const { t } = useTranslationContext('MessageOptions');
61+
5962
const handleOpenThread = propHandleOpenThread || contextHandleOpenThread;
6063

6164
const messageActions = getMessageActions();
@@ -87,7 +90,7 @@ const UnMemoizedMessageOptions = <
8790
)}
8891
{shouldShowReplies && (
8992
<button
90-
aria-label='Open Thread'
93+
aria-label={t('aria/Open Thread')}
9194
className={`str-chat__message-${theme}__actions__action str-chat__message-${theme}__actions__action--thread str-chat__message-reply-in-thread-button`}
9295
data-testid='thread-action'
9396
onClick={handleOpenThread}
@@ -98,7 +101,7 @@ const UnMemoizedMessageOptions = <
98101
{shouldShowReactions && (
99102
<button
100103
aria-expanded={showDetailedReactions}
101-
aria-label='Open Reaction Selector'
104+
aria-label={t('aria/Open Reaction Selector')}
102105
className={`str-chat__message-${theme}__actions__action str-chat__message-${theme}__actions__action--reactions str-chat__message-reactions-button`}
103106
data-testid='message-reaction-action'
104107
onClick={onReactionListClick}

0 commit comments

Comments
 (0)