Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make aria-labels localizable #2282

Merged
merged 16 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 47 additions & 11 deletions docusaurus/docs/React/guides/theming/translations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,40 @@ JSON objects can be imported from the library.
`import { esTranslations } from 'stream-chat-react';`
:::

### Overriding ARIA labels

ARIA labels that are used for interactive elements are also subject to localization. Translation keys for ARIA labels are prefixed by `aria/`:

```jsx
import { useTranslationContext } from 'stream-chat-react';

const Component = () => {
const { t } = useTranslationContext();
return (
<button type='button' aria-label={t('aria/Send')}>
📨
</button>
);
};
```

To override the default translations, pass an `aria` object inside the `translationsForLanguage` object:

```jsx
const i18nInstance = new Streami18n({
language: 'en',
translationsForLanguage: {
aria: {
Send: 'Send Message',
},
},
});

<Chat client={client} i18nInstance={i18nInstance}>
{/* ... */}
</Chat>;
```

## Add a Language

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

const streamI18n = new Streami18n({ timezone: 'Europe/Prague'});
const streamI18n = new Streami18n({ timezone: 'Europe/Prague' });
```

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.
Expand All @@ -305,7 +339,7 @@ import { Streami18n } from 'stream-chat-react';
const i18n = new Streami18n({
DateTimeParser: momentTimezone,
timezone: 'Europe/Prague',
})
});
```

### Translating Messages
Expand All @@ -331,7 +365,7 @@ The `Streami18n` class wraps [`i18next`](https://www.npmjs.com/package/i18next)
### Class Constructor Options

| Option | Description | Type | Default |
|------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------------------------|
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------- |
| DateTimeParser | custom date time parser | function | Day.js |
| 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' |
| debug | enables i18n debug mode | boolean | false |
Expand All @@ -350,21 +384,23 @@ The default implementation returns the default value provided to the translator
import { useTranslationContext } from 'stream-chat-react';

const Component = () => {
const { t } = useTranslationContext('useCommandTrigger');
const { t } = useTranslationContext('useCommandTrigger');

return (
<div>{t('some-key', {defaultValue: 'hello'})}</div>
);
}
return <div>{t('some-key', { defaultValue: 'hello' })}</div>;
};
```

The custom handler may log missing key warnings to the console in the development environment:

```ts
import { Streami18n, Streami18nOptions } from 'stream-chat-react';

const parseMissingKeyHandler: Streami18nOptions['parseMissingKeyHandler'] = (key: string, defaultValue?: string) => {
console.warn(`Streami18n: Missing translation for key: ${key}`);
return defaultValue ?? key;
const parseMissingKeyHandler: Streami18nOptions['parseMissingKeyHandler'] = (
key: string,
defaultValue?: string,
) => {
console.warn(`Streami18n: Missing translation for key: ${key}`);
return defaultValue ?? key;
};

const i18nInstance = new Streami18n({ parseMissingKeyHandler });
Expand Down
2 changes: 1 addition & 1 deletion i18next-parser.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module.exports = {
createOldCatalogs: false,
input: ['./src/**/*.{tsx,ts}'],
keepRemoved: true,
keySeparator: false,
keySeparator: '/',
locales: ['de', 'en', 'es', 'fr', 'hi', 'it', 'ja', 'ko', 'nl', 'pt', 'ru', 'tr'],
namespaceSeparator: false,
output: 'src/i18n/$LOCALE.json',
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"isomorphic-ws": "^4.0.1",
"linkifyjs": "^4.1.0",
"lodash.debounce": "^4.0.8",
"lodash.defaultsdeep": "^4.6.1",
"lodash.throttle": "^4.1.1",
"lodash.uniqby": "^4.7.0",
"nanoid": "^3.3.4",
Expand Down Expand Up @@ -154,6 +155,7 @@
"@types/jsdom": "^21.1.5",
"@types/linkifyjs": "^2.1.3",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.defaultsdeep": "^4.6.9",
"@types/lodash.throttle": "^4.1.7",
"@types/lodash.uniqby": "^4.7.7",
"@types/moment": "^2.13.0",
Expand Down
14 changes: 9 additions & 5 deletions src/components/Attachment/__tests__/Card.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import {
generateUser,
getOrCreateChannelApi,
getTestClientWithUser,
mockTranslationContext,
useMockedApis,
} from '../../../mock-builders';
import { TranslationContext } from '../../../context';

let chatClient;
let channel;
Expand All @@ -31,11 +33,13 @@ const mockedChannel = generateChannel({
const renderCard = ({ cardProps, chatContext, theRenderer = render }) =>
theRenderer(
<ChatProvider value={{ themeVersion: '1', ...chatContext }}>
<ChannelStateProvider value={{}}>
<ComponentProvider value={{}}>
<Card {...cardProps} />
</ComponentProvider>
</ChannelStateProvider>
<TranslationContext.Provider value={mockTranslationContext}>
<ChannelStateProvider value={{}}>
<ComponentProvider value={{}}>
<Card {...cardProps} />
</ComponentProvider>
</ChannelStateProvider>
</TranslationContext.Provider>
</ChatProvider>,
);

Expand Down
6 changes: 5 additions & 1 deletion src/components/Attachment/__tests__/File.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import renderer from 'react-test-renderer';
import { FileAttachment } from '../FileAttachment';

import { ChatContext } from '../../../context/ChatContext';
import { TranslationContext } from '../../../context';
import { mockTranslationContext } from '../../../mock-builders';

const getComponent = ({ attachment, chatContext }) => (
<ChatContext.Provider value={chatContext}>
<FileAttachment attachment={attachment} />
<TranslationContext.Provider value={mockTranslationContext}>
<FileAttachment attachment={attachment} />
</TranslationContext.Provider>
</ChatContext.Provider>
);

Expand Down
6 changes: 5 additions & 1 deletion src/components/ChannelHeader/ChannelHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ const UnMemoizedChannelHeader = <

return (
<div className='str-chat__header-livestream str-chat__channel-header'>
<button aria-label='Menu' className='str-chat__header-hamburger' onClick={openMobileNav}>
<button
aria-label={t('aria/Menu')}
className='str-chat__header-hamburger'
onClick={openMobileNav}
>
<MenuIcon />
</button>
<Avatar
Expand Down
4 changes: 3 additions & 1 deletion src/components/ChannelList/ChannelListMessenger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { LoadingChannels } from '../Loading/LoadingChannels';
import type { APIErrorResponse, Channel, ErrorFromResponse } from 'stream-chat';

import type { DefaultStreamChatGenerics } from '../../types/types';
import { useTranslationContext } from '../../context';

export type ChannelListMessengerProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
Expand Down Expand Up @@ -39,6 +40,7 @@ export const ChannelListMessenger = <
LoadingErrorIndicator = ChatDown,
LoadingIndicator = LoadingChannels,
} = props;
const { t } = useTranslationContext('ChannelListMessenger');

if (error) {
return <LoadingErrorIndicator type='Connection Error' />;
Expand All @@ -51,7 +53,7 @@ export const ChannelListMessenger = <
return (
<div className='str-chat__channel-list-messenger str-chat__channel-list-messenger-react'>
<div
aria-label='Channel list'
aria-label={t('aria/Channel list')}
className='str-chat__channel-list-messenger__main str-chat__channel-list-messenger-react__main'
role='listbox'
>
Expand Down
23 changes: 15 additions & 8 deletions src/components/ChannelList/__tests__/ChannelList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@ import {
ChannelPreviewMessenger,
} from '../../ChannelPreview';

import { ChatContext, useChannelListContext, useChatContext } from '../../../context';
import {
ChatContext,
TranslationContext,
useChannelListContext,
useChatContext,
} from '../../../context';
import { ChannelListMessenger } from '../ChannelListMessenger';
import { initClientWithChannels } from '../../../mock-builders';
import { initClientWithChannels, mockTranslationContext } from '../../../mock-builders';

expect.extend(toHaveNoViolations);

Expand Down Expand Up @@ -603,12 +608,14 @@ describe('ChannelList', () => {
...chatContext,
}}
>
<ChannelList
filters={{}}
options={{ presence: true, state: true }}
showChannelSearch
{...channeListProps}
/>
<TranslationContext.Provider value={mockTranslationContext}>
<ChannelList
filters={{}}
options={{ presence: true, state: true }}
showChannelSearch
{...channeListProps}
/>
</TranslationContext.Provider>
</ChatContext.Provider>,
);

Expand Down
22 changes: 13 additions & 9 deletions src/components/ChannelList/__tests__/ChannelListMessenger.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@ import '@testing-library/jest-dom';
import renderer from 'react-test-renderer';

import { ChannelListMessenger } from '../ChannelListMessenger';
import { TranslationProvider } from '../../../context';
import { mockTranslationContext } from '../../../mock-builders';

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

const Component = ({ error = false, loading = false }) => (
<ChannelListMessenger
error={error}
loading={loading}
LoadingErrorIndicator={() => <div>Loading Error Indicator</div>}
LoadingIndicator={() => <div>Loading Indicator</div>}
>
<div>children 1</div>
<div>children 2</div>
</ChannelListMessenger>
<TranslationProvider value={mockTranslationContext}>
<ChannelListMessenger
error={error}
loading={loading}
LoadingErrorIndicator={() => <div>Loading Error Indicator</div>}
LoadingIndicator={() => <div>Loading Indicator</div>}
>
<div>children 1</div>
<div>children 2</div>
</ChannelListMessenger>
</TranslationProvider>
);

describe('ChannelListMessenger', () => {
Expand Down
26 changes: 15 additions & 11 deletions src/components/ChannelSearch/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,17 +142,21 @@ const DefaultSearchResultItem = <
const ResultsContainer = ({
children,
popupResults,
}: PropsWithChildren<{ popupResults?: boolean }>) => (
<div
aria-label='Channel search results'
className={clsx(
`str-chat__channel-search-container str-chat__channel-search-result-list`,
popupResults ? 'popup' : 'inline',
)}
>
{children}
</div>
);
}: PropsWithChildren<{ popupResults?: boolean }>) => {
const { t } = useTranslationContext('ResultsContainer');

return (
<div
aria-label={t('aria/Channel search results')}
className={clsx(
`str-chat__channel-search-container str-chat__channel-search-result-list`,
popupResults ? 'popup' : 'inline',
)}
>
{children}
</div>
);
};

export type SearchResultsController<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
Expand Down
2 changes: 1 addition & 1 deletion src/components/Emojis/EmojiPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export const EmojiPicker = (props: EmojiPickerProps) => {
)}
<button
aria-expanded={displayPicker}
aria-label='Emoji picker'
aria-label={t('aria/Emoji picker')}
className={props.buttonClassName ?? buttonClassName}
onClick={() => setDisplayPicker((cv) => !cv)}
ref={setReferenceElement}
Expand Down
6 changes: 3 additions & 3 deletions src/components/Gallery/__tests__/BaseImage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import '@testing-library/jest-dom';

import { BaseImage } from '../BaseImage';
import { TranslationProvider } from '../../../context';
import { mockTranslationContext } from '../../../mock-builders';

const props = {
alt: 'alt',
src: 'src',
};
const t = (val) => val;
const BASE_IMAGE_TEST_ID = 'str-chat__base-image';
const getImage = () => screen.queryByTestId(BASE_IMAGE_TEST_ID);

const renderComponent = (props = {}) =>
render(
<TranslationProvider value={{ t }}>
<TranslationProvider value={mockTranslationContext}>
<BaseImage {...props} />
</TranslationProvider>,
);
Expand Down Expand Up @@ -92,7 +92,7 @@ describe('BaseImage', () => {
fireEvent.error(getImage());

rerender(
<TranslationProvider value={{ t }}>
<TranslationProvider value={mockTranslationContext}>
<BaseImage src={'new-src'} />
</TranslationProvider>,
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/LoadMore/LoadMoreButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const UnMemoizedLoadMoreButton = ({
return (
<div className='str-chat__load-more-button'>
<button
aria-label='Load More Channels'
aria-label={t('aria/Load More Channels')}
className='str-chat__load-more-button__button str-chat__cta-button'
data-testid='load-more-button'
disabled={loading}
Expand Down
8 changes: 7 additions & 1 deletion src/components/LoadMore/__tests__/LoadMoreButton.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import renderer from 'react-test-renderer';
import '@testing-library/jest-dom';

import { LoadMoreButton } from '../LoadMoreButton';
import { TranslationProvider } from '../../../context';
import { mockTranslationContext } from '../../../mock-builders';

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

it('should render component with default props', () => {
const tree = renderer
.create(<LoadMoreButton isLoading={false} onClick={() => null} />)
.create(
<TranslationProvider value={mockTranslationContext}>
<LoadMoreButton isLoading={false} onClick={() => null} />
</TranslationProvider>,
)
.toJSON();
expect(tree).toMatchInlineSnapshot(`
<div
Expand Down
Loading