From 7867677f2b17d3649324cc80f43c459e9b30bf95 Mon Sep 17 00:00:00 2001 From: Matvei Andrienko Date: Wed, 21 Feb 2024 10:55:43 +0100 Subject: [PATCH] feat: make aria-labels localizable (#2282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 🎯 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 --- .../React/guides/theming/translations.mdx | 56 +++++++++++++++---- .../Attachment/__tests__/Card.test.js | 14 +++-- .../Attachment/__tests__/File.test.js | 6 +- .../ChannelHeader/ChannelHeader.tsx | 6 +- .../ChannelList/ChannelListMessenger.tsx | 4 +- .../ChannelList/__tests__/ChannelList.test.js | 23 +++++--- .../__tests__/ChannelListMessenger.test.js | 22 +++++--- .../ChannelSearch/SearchResults.tsx | 26 +++++---- src/components/Emojis/EmojiPicker.tsx | 2 +- .../Gallery/__tests__/BaseImage.test.js | 6 +- src/components/LoadMore/LoadMoreButton.tsx | 2 +- .../LoadMore/__tests__/LoadMoreButton.test.js | 8 ++- src/components/Message/MessageOptions.tsx | 7 ++- .../MessageActions/MessageActions.tsx | 5 +- .../MessageActions/MessageActionsBox.tsx | 6 +- .../__tests__/MessageActions.test.js | 4 +- .../MessageInput/MessageInputFlat.tsx | 2 +- .../MessageInput/QuotedMessagePreview.tsx | 2 +- src/components/MessageInput/icons.tsx | 3 +- .../ReactFileUtilities/FileUploadButton.tsx | 4 +- .../ReactFileUtilities/IconButton.tsx | 26 +++++---- .../ReactFileUtilities/ImagePreviewer.tsx | 5 +- .../ReactFileUtilities/ImageUploadButton.tsx | 36 ++++++------ src/components/Reactions/ReactionsList.tsx | 5 +- src/components/SafeAnchor/SafeAnchor.tsx | 4 +- src/components/Thread/ThreadHeader.tsx | 2 +- src/i18n/__tests__/Streami18n.test.js | 12 +++- src/i18n/de.json | 19 +++++++ src/i18n/en.json | 19 +++++++ src/i18n/es.json | 19 +++++++ src/i18n/fr.json | 19 +++++++ src/i18n/hi.json | 19 +++++++ src/i18n/it.json | 19 +++++++ src/i18n/ja.json | 19 +++++++ src/i18n/ko.json | 19 +++++++ src/i18n/nl.json | 19 +++++++ src/i18n/pt.json | 19 +++++++ src/i18n/ru.json | 19 +++++++ src/i18n/tr.json | 19 +++++++ src/mock-builders/translator.js | 6 ++ 40 files changed, 434 insertions(+), 98 deletions(-) diff --git a/docusaurus/docs/React/guides/theming/translations.mdx b/docusaurus/docs/React/guides/theming/translations.mdx index a0e8d2bf2..7da66e194 100644 --- a/docusaurus/docs/React/guides/theming/translations.mdx +++ b/docusaurus/docs/React/guides/theming/translations.mdx @@ -132,6 +132,38 @@ 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 ( + + ); +}; +``` + +To override the default translations, add an `aria`-prefixed key to the `translationsForLanguage` object: + +```jsx +const i18nInstance = new Streami18n({ + language: 'en', + translationsForLanguage: { + 'aria/Send': 'Send Message', + }, +}); + + + {/* ... */} +; +``` + ## Add a Language 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 ```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. @@ -305,7 +337,7 @@ import { Streami18n } from 'stream-chat-react'; const i18n = new Streami18n({ DateTimeParser: momentTimezone, timezone: 'Europe/Prague', -}) +}); ``` ### Translating Messages @@ -331,7 +363,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 | @@ -350,21 +382,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 ( -
{t('some-key', {defaultValue: 'hello'})}
- ); -} + return
{t('some-key', { defaultValue: 'hello' })}
; +}; ``` + 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 }); diff --git a/src/components/Attachment/__tests__/Card.test.js b/src/components/Attachment/__tests__/Card.test.js index 022bceab6..4b2b1072b 100644 --- a/src/components/Attachment/__tests__/Card.test.js +++ b/src/components/Attachment/__tests__/Card.test.js @@ -15,8 +15,10 @@ import { generateUser, getOrCreateChannelApi, getTestClientWithUser, + mockTranslationContext, useMockedApis, } from '../../../mock-builders'; +import { TranslationContext } from '../../../context'; let chatClient; let channel; @@ -31,11 +33,13 @@ const mockedChannel = generateChannel({ const renderCard = ({ cardProps, chatContext, theRenderer = render }) => theRenderer( - - - - - + + + + + + + , ); diff --git a/src/components/Attachment/__tests__/File.test.js b/src/components/Attachment/__tests__/File.test.js index 0a80bcdc1..5f5f75394 100644 --- a/src/components/Attachment/__tests__/File.test.js +++ b/src/components/Attachment/__tests__/File.test.js @@ -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 }) => ( - + + + ); diff --git a/src/components/ChannelHeader/ChannelHeader.tsx b/src/components/ChannelHeader/ChannelHeader.tsx index 16b9b114f..fbb428ec4 100644 --- a/src/components/ChannelHeader/ChannelHeader.tsx +++ b/src/components/ChannelHeader/ChannelHeader.tsx @@ -50,7 +50,11 @@ const UnMemoizedChannelHeader = < return (
- ; @@ -51,7 +53,7 @@ export const ChannelListMessenger = < return (
diff --git a/src/components/ChannelList/__tests__/ChannelList.test.js b/src/components/ChannelList/__tests__/ChannelList.test.js index 608fff291..2552dc784 100644 --- a/src/components/ChannelList/__tests__/ChannelList.test.js +++ b/src/components/ChannelList/__tests__/ChannelList.test.js @@ -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); @@ -603,12 +608,14 @@ describe('ChannelList', () => { ...chatContext, }} > - + + + , ); diff --git a/src/components/ChannelList/__tests__/ChannelListMessenger.test.js b/src/components/ChannelList/__tests__/ChannelListMessenger.test.js index e9f7432fd..804f01dbe 100644 --- a/src/components/ChannelList/__tests__/ChannelListMessenger.test.js +++ b/src/components/ChannelList/__tests__/ChannelListMessenger.test.js @@ -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 }) => ( -
Loading Error Indicator
} - LoadingIndicator={() =>
Loading Indicator
} - > -
children 1
-
children 2
-
+ +
Loading Error Indicator
} + LoadingIndicator={() =>
Loading Indicator
} + > +
children 1
+
children 2
+
+
); describe('ChannelListMessenger', () => { diff --git a/src/components/ChannelSearch/SearchResults.tsx b/src/components/ChannelSearch/SearchResults.tsx index 9cdd24fd8..44ebb0ebb 100644 --- a/src/components/ChannelSearch/SearchResults.tsx +++ b/src/components/ChannelSearch/SearchResults.tsx @@ -142,17 +142,21 @@ const DefaultSearchResultItem = < const ResultsContainer = ({ children, popupResults, -}: PropsWithChildren<{ popupResults?: boolean }>) => ( -
- {children} -
-); +}: PropsWithChildren<{ popupResults?: boolean }>) => { + const { t } = useTranslationContext('ResultsContainer'); + + return ( +
+ {children} +
+ ); +}; export type SearchResultsController< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics diff --git a/src/components/Emojis/EmojiPicker.tsx b/src/components/Emojis/EmojiPicker.tsx index 247bf1363..247a02f22 100644 --- a/src/components/Emojis/EmojiPicker.tsx +++ b/src/components/Emojis/EmojiPicker.tsx @@ -101,7 +101,7 @@ export const EmojiPicker = (props: EmojiPickerProps) => { )} -); +export const IconButton = ({ children, onClick }: PropsWithChildren) => { + const { t } = useTranslationContext('IconButton'); + return ( + + ); +}; diff --git a/src/components/ReactFileUtilities/ImagePreviewer.tsx b/src/components/ReactFileUtilities/ImagePreviewer.tsx index 74c925b61..89c636939 100644 --- a/src/components/ReactFileUtilities/ImagePreviewer.tsx +++ b/src/components/ReactFileUtilities/ImagePreviewer.tsx @@ -7,6 +7,7 @@ import { RetryIcon } from './icons'; import type { ImageUpload } from './types'; import clsx from 'clsx'; +import { useTranslationContext } from '../../context'; type CustomMouseEvent = (id: string, event: MouseEvent) => void; @@ -34,6 +35,8 @@ export const ImagePreviewer = ({ imageUploads, multiple = true, }: ImagePreviewerProps) => { + const { t } = useTranslationContext('ImagePreviewer'); + const onClose: CustomMouseEvent = useCallback( (id, event) => { if (!id) return console.warn(`image.id of closed image was "null", this shouldn't happen`); @@ -56,7 +59,7 @@ export const ImagePreviewer = ({ > {image.state === 'failed' && (