diff --git a/.claude/skills/accessibility/SKILL.md b/.claude/skills/accessibility/SKILL.md index ab32176cce..fe57c6a277 100644 --- a/.claude/skills/accessibility/SKILL.md +++ b/.claude/skills/accessibility/SKILL.md @@ -10,7 +10,7 @@ Use this skill whenever code changes can affect screen-reader users (VoiceOver o ## Non-negotiable rules 1. **Native semantics first.** Use `Pressable`, `TextInput`, `Switch`, `Image` directly. Use `accessibilityRole` only when native semantics cannot represent the widget (`menu`, `menuitem`, `progressbar`, `radio`, `checkbox`, `article`, `alert`, `tablist`, `tab`). -2. **Never hardcode English** in `accessibilityLabel`/`accessibilityHint`/announcement strings. For SDK `Button`, pass `accessibilityLabelKey='a11y/...'` (and `accessibilityLabelParams` when needed). For non-Button components, use `useA11yLabel('a11y/...', params)` or `t('a11y/...')` directly when you don't need the disabled-state short-circuit. Add the key to all 12 locale files in `package/src/i18n/`. +2. **Never hardcode English** in `accessibilityLabel`/`accessibilityHint`/announcement strings. For SDK `Button`, pass `accessibilityLabelKey='a11y/...'` (and `accessibilityLabelParams` when needed). For non-Button components, use `useA11yLabel('a11y/...', params)` or `t('a11y/...')` directly when you don't need the disabled-state short-circuit. Add the key to all 13 locale files in `package/src/i18n/` (`ar, en, es, fr, he, hi, it, ja, ko, nl, pt-br, ru, tr`). 3. **Gate behavior on `useAccessibilityContext().enabled`.** A11y is opt-in. New listeners, subscriptions, and announcer mounts must be no-ops when `enabled` is false. New `accessibilityRole`/`accessibilityState` props are fine to render unconditionally — they cost ~zero. 4. **One focusable target per action.** Don't nest `Pressable` inside `Pressable`. Mark inner decorative views with `accessibilityElementsHidden` (iOS) + `importantForAccessibility='no-hide-descendants'` (Android) so the parent carries the label. 5. **Decorative visuals stay hidden from AT.** Icon-only buttons must carry an `accessibilityLabel` on the wrapper, and the SVG icon should be hidden. @@ -21,7 +21,7 @@ Use this skill whenever code changes can affect screen-reader users (VoiceOver o - **Foundation primitives** → `package/src/a11y/` (utilities + low-level hooks). - **Runtime announcer infra** → `package/src/components/Accessibility/` (`NotificationAnnouncer`, `useAccessibilityAnnouncer`, `useIncomingMessageAnnouncements`). - **Config + provider** → `package/src/contexts/accessibilityContext/`, mounted by `OverlayProvider`. -- **i18n** → `a11y/*` keys in all 12 locale JSONs (`en, es, fr, he, hi, it, ja, ko, nl, pt-br, ru, tr`). +- **i18n** → `a11y/*` keys in all 13 locale JSONs (`ar, en, es, fr, he, hi, it, ja, ko, nl, pt-br, ru, tr`). - **Component-level a11y attributes** → in the component itself. - **Platform divergence (iOS vs Android)** → use `Platform.OS` or `useResolvedModalAccessibilityProps`. Don't duplicate the file — RN doesn't need `.ios.tsx`/`.android.tsx` splits for a11y. - **Tests** → nearest `__tests__/` folder; use `@testing-library/react-native` semantic queries (`getByRole`, `getByLabelText`). @@ -97,6 +97,81 @@ const transitionDuration = reduceMotion ? 0 : 250; Disable spring animations and limit fade durations when this is true. +### 6) Curated single focus stop for visual content — `CompositeAccessibilityProbe` + +```tsx +import { CompositeAccessibilityProbe } from 'stream-chat-react-native'; + + + {/* avatars, icons, composed graphics — visually decorative */} + +``` + +Wraps non-Text visual content with a single, cross-platform-stable focus stop carrying the provided `label`. Renders a hidden `Text` sibling that carries the label + a `View accessibilityElementsHidden importantForAccessibility='no-hide-descendants'` around the children. Use for avatars, mute icons, isolated badges, composed graphics that should announce as one semantic unit. + +Pass the result of `useA11yLabel(...)` directly — when `label` is `undefined` (a11y opt-out), the probe is a no-op and renders children untouched. + +Live examples: `ChannelAvatar.tsx`, `ChannelPreviewMutedStatus.tsx`, `ChannelMessagePreviewDeliveryStatus.tsx`. + +### 7) Splicing extra a11y info into compose — `HiddenA11yText` + +```tsx +import { HiddenA11yText } from 'stream-chat-react-native'; + + + + {selected ? : null} + +``` + +A visually-invisible `` that exists only to contribute extra information to a parent's compose loop. Use it to splice in supplementary state ("you reacted", "and N more", "unread") that doesn't have a natural visible Text in the tree. + +Different concern from `CompositeAccessibilityProbe`: +- `HiddenA11yText` — "inject extra a11y-only text into a compose chain" +- `CompositeAccessibilityProbe` — "make this whole visual element one focus stop with a curated label" + +Live examples: `MessageStatus.tsx`, `ReactionListClustered.tsx`, `ReactionListItem.tsx`. + +### 8) Cross-platform auto-compose on a plain View + +```tsx + + {/* children whose labels should auto-compose into one announcement */} + +``` + +iOS auto-composes descendant labels when a `View` is `accessible={true}` without an explicit `accessibilityLabel`. Android requires the parent to trip a gate — set any of `accessibilityRole`, `accessibilityState`, `accessibilityActions`, or `accessibilityLabelledBy`. `accessibilityRole='text'` (or `'none'`) is the lightest gate-tripper and a no-op for iOS composition. + +`Pressable` defaults `accessibilityRole='button'`, so it auto-trips the gate. Plain `View accessible={true}` without a role does NOT — Android falls back to its default heuristic (reads one visible Text descendant only). + +Live example: `MessageFooter.tsx` — `` makes the footer one focus stop on both platforms reading `"Read 11:05 AM"`. + +See full memory: `rn_android_a11y_compose_gate.md`. + +### 9) Drill-in for interactive children inside a Pressable + +```tsx + + {/* mix of interactive children — attachments, quoted reply, poll options, etc. */} + +``` + +When a Pressable wraps mixed content that includes interactive children, the row's default single-focus-stop behavior subsumes them — screen-reader users can't activate the children individually. Setting `accessible={false}` on the Pressable removes the row stop, so VO/TalkBack drill into each interactive child. The Pressable's `onPress` / `onLongPress` still fire because VO/TalkBack synthesize taps at the focused child's coordinates, which land inside the Pressable's hit area. + +Live example: `MessageContent.tsx` — `accessible={hasInteractiveContent ? false : undefined}` where `hasInteractiveContent` covers poll, quoted message, attachments, shared location. + +### 10) Reshow announcements — `useAnnounceOnShow` + +```tsx +useAnnounceOnShow(visible, useA11yLabel('a11y/Replying to {{user}}', { user: name })); +``` + +Announces `label` once each time `visible` flips from `false` to `true`. Resets on hide, so reshows re-announce — unlike `useAnnounceOnStateChange` which dedupes consecutive identical strings. + +Use for transient surfaces that appear and disappear repeatedly within a session (modals, autocomplete pickers, reply previews) where the user benefits from hearing the affordance on every reappearance. + +Live example: `Reply.tsx` — fires when a reply preview shows in the composer. + ## Anti-patterns to avoid - **Hardcoded English `accessibilityLabel`** strings inside component code. For SDK `Button`, use `accessibilityLabelKey='a11y/...'`; otherwise use `useA11yLabel('a11y/...')` or `t('a11y/...')`. @@ -134,11 +209,17 @@ Recommended for non-trivial changes: - `package/src/contexts/accessibilityContext/AccessibilityContext.tsx` — config schema + provider + imperative announcer context. - `package/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts` — port of stream-chat-react's hook. +- `package/src/components/Accessibility/CompositeAccessibilityProbe.tsx` — curated-single-focus-stop wrapper for visual content (avatar, icons, badges). +- `package/src/components/Accessibility/HiddenA11yText.tsx` — visually-invisible Text that splices extra info into a parent's compose chain ("you reacted", "and N more", etc). - `package/src/a11y/hooks/useA11yLabel.ts` — translated-label-or-undefined. +- `package/src/a11y/hooks/useAnnounceOnStateChange.ts` — announce on string-change with dedup. +- `package/src/a11y/hooks/useAnnounceOnShow.ts` — announce on `visible: false → true` transitions, resets on hide (no dedup). - `package/src/a11y/hooks/useResolvedModalAccessibilityProps.ts` — modal a11y props. - `package/src/components/ui/Avatar/Avatar.tsx` — example of `name` + `useA11yLabel` usage. - `package/src/components/UIComponents/BottomSheetModal.tsx` — example of `useResolvedModalAccessibilityProps`. - `package/src/components/AITypingIndicatorView/AITypingIndicatorView.tsx` — example of `useAnnounceOnStateChange`. +- `package/src/components/Message/MessageItemView/MessageFooter.tsx` — example of cross-platform auto-compose on a View (`accessible + accessibilityRole='text'`). +- `package/src/components/Message/MessageItemView/MessageContent.tsx` — example of conditional drill-in (`accessible={hasInteractiveContent ? false : undefined}`). ## Cross-SDK parity diff --git a/package/src/a11y/hooks/useAnnounceOnShow.ts b/package/src/a11y/hooks/useAnnounceOnShow.ts new file mode 100644 index 0000000000..d186c9af1b --- /dev/null +++ b/package/src/a11y/hooks/useAnnounceOnShow.ts @@ -0,0 +1,44 @@ +import { useEffect, useRef } from 'react'; + +import { useAccessibilityAnnouncer } from '../../components/Accessibility/useAccessibilityAnnouncer'; + +type Options = { + /** Delay before the announcement fires; lets entrance animations settle. */ + delayMs?: number; + priority?: 'polite' | 'assertive'; +}; + +/** + * Announces `message` once each time `visible` flips from false to true. + * Resets when `visible` flips back to false, so the next show re-announces — + * unlike `useAnnounceOnStateChange`, which announces on string change and + * dedupes consecutive identical strings. + * + * Use this for transient surfaces that appear and disappear repeatedly within + * a session (modals, autocomplete pickers, bottom sheets) where the user + * benefits from hearing the affordance on every reappearance. + * + * When `message` is undefined (typically because `useA11yLabel` returned + * undefined — a11y disabled or key missing), the hook is a no-op. + */ +export const useAnnounceOnShow = ( + visible: boolean, + message: string | undefined, + { delayMs = 500, priority = 'polite' }: Options = {}, +) => { + const announce = useAccessibilityAnnouncer(); + const announcedRef = useRef(false); + + useEffect(() => { + if (!visible) { + announcedRef.current = false; + return; + } + if (!message || announcedRef.current) return; + const id = setTimeout(() => { + announce(message, priority); + announcedRef.current = true; + }, delayMs); + return () => clearTimeout(id); + }, [visible, message, announce, priority, delayMs]); +}; diff --git a/package/src/a11y/index.ts b/package/src/a11y/index.ts index 46279098ce..ffbd8b290f 100644 --- a/package/src/a11y/index.ts +++ b/package/src/a11y/index.ts @@ -3,5 +3,6 @@ export * from './hooks/useScreenReaderEnabled'; export * from './hooks/useReducedMotionPreference'; export * from './hooks/useResolvedModalAccessibilityProps'; export * from './hooks/useAnnounceOnStateChange'; +export * from './hooks/useAnnounceOnShow'; export * from './hooks/useA11yLabel'; export * from './hooks/useAccessibilityActivateAction'; diff --git a/package/src/components/Accessibility/CompositeAccessibilityProbe.tsx b/package/src/components/Accessibility/CompositeAccessibilityProbe.tsx new file mode 100644 index 0000000000..27acd8364c --- /dev/null +++ b/package/src/components/Accessibility/CompositeAccessibilityProbe.tsx @@ -0,0 +1,48 @@ +import React, { PropsWithChildren } from 'react'; +import { View } from 'react-native'; + +import { HiddenA11yText } from './HiddenA11yText'; + +export type CompositeAccessibilityProbeProps = { + /** + * The accessibility label that VoiceOver / TalkBack should announce for the + * wrapped content. When `undefined`, the probe is a no-op and the children + * render with no a11y modifications — use this to skip the wrapper when the + * SDK's a11y opt-in is off. + */ + label: string | undefined; +}; + +/** + * Wraps decorative visual content with a single, cross platform stable + * accessibility focus stop carrying the provided `label`. + * + * iOS auto collapses descendants when a parent View is `accessible`, but on + * Android `importantForAccessibility='no-hide-descendants'` on the parent + * gets defeated by deeply nested descendants that set their own + * `accessible={true}` (our SDK's `` does this). A zero size accessible + * `` sidesteps that - Text is always accessible by default on both + * platforms and carries the label cleanly, while the visual subtree is + * marked decorative. More importantly, it's discoverable very very easily + * by screen readers. + * + * Use this anywhere you have non-Text visual content (avatars, icons, + * composed graphics) that should announce as a single semantic unit with a + * curated label, rather than letting screen readers walk the visual tree + * verbosely. + */ +export const CompositeAccessibilityProbe = ({ + children, + label, +}: PropsWithChildren) => { + if (!label) return <>{children}; + + return ( + <> + + + {children} + + + ); +}; diff --git a/package/src/components/Accessibility/HiddenA11yText.tsx b/package/src/components/Accessibility/HiddenA11yText.tsx new file mode 100644 index 0000000000..02200ccabd --- /dev/null +++ b/package/src/components/Accessibility/HiddenA11yText.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { StyleSheet, Text } from 'react-native'; + +export type HiddenA11yTextProps = { + /** + * The text to inject into the accessibility tree. Rendered as actual Text + * content (not as `accessibilityLabel`) so the parent's compose loop on + * Android picks it up — Text without its own label isn't + * `isAccessibilityFocusable`, so it gets concatenated into the parent's + * announcement rather than being skipped as a drill-in target. + * + * Pass the result of `useA11yLabel(...)` directly: when SDK a11y is + * opt-out the value is `undefined` and the component renders nothing. + */ + label: string | undefined; +}; + +/** + * A visually invisible Text that exists only to contribute extra information + * to a screen reader's announcement. Use it inside a parent that auto-composes + * descendant labels (Pressable, or any View with `accessible` + `accessibilityRole`) + * to splice in supplementary state like "you reacted", "and N more", etc. + * + * Not for "this whole element should be one focus stop with a curated label" - + * use `CompositeAccessibilityProbe` for that. + */ +export const HiddenA11yText = ({ label }: HiddenA11yTextProps) => { + if (!label) return null; + // Both content and accessibilityLabel are set to the same string. Content + // keeps the Text on the parent's compose loop (label-only would make it + // `isAccessibilityFocusable` and potentially skipped on Android — though + // the opacity:0 hidden style usually saves it). accessibilityLabel keeps + // testing-library `getByLabelText(...)` queries working. + return ( + + {label} + + ); +}; + +const styles = StyleSheet.create({ + hidden: { + height: 1, + opacity: 0, + overflow: 'hidden', + position: 'absolute', + width: 1, + }, +}); diff --git a/package/src/components/Accessibility/index.ts b/package/src/components/Accessibility/index.ts index 14d86325c1..612ce7a92a 100644 --- a/package/src/components/Accessibility/index.ts +++ b/package/src/components/Accessibility/index.ts @@ -1,3 +1,5 @@ +export * from './CompositeAccessibilityProbe'; +export * from './HiddenA11yText'; export * from './NotificationAnnouncer'; export * from './useAccessibilityAnnouncer'; export * from './hooks/useIncomingMessageAnnouncements'; diff --git a/package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx b/package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx index 642fc565ca..928b0ab77b 100644 --- a/package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx +++ b/package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx @@ -13,6 +13,7 @@ import { MessageDeliveryStatus, useMessageDeliveryStatus } from '../../hooks'; import { Check, CheckAll, Time } from '../../icons'; import { primitives } from '../../theme'; import { MessageStatusTypes } from '../../utils/utils'; +import { CompositeAccessibilityProbe } from '../Accessibility/CompositeAccessibilityProbe'; export type ChannelMessagePreviewDeliveryStatusProps = Pick & { message: MessageResponse | LocalMessage; @@ -66,11 +67,11 @@ export const ChannelMessagePreviewDeliveryStatus = ({ message.status === MessageStatusTypes.SENDING ? 'a11y/Sending' : message.status === MessageStatusTypes.RECEIVED && status === MessageDeliveryStatus.READ - ? 'a11y/Read' + ? 'a11y/Read, sent by you' : status === MessageDeliveryStatus.DELIVERED - ? 'a11y/Delivered' + ? 'a11y/Delivered, sent by you' : status === MessageDeliveryStatus.SENT - ? 'a11y/Sent' + ? 'a11y/Sent by you' : 'a11y/Sending', ); @@ -83,19 +84,21 @@ export const ChannelMessagePreviewDeliveryStatus = ({ } return ( - - {message.status === MessageStatusTypes.SENDING ? ( - + + + {message.status === MessageStatusTypes.SENDING ? ( + + ); }; diff --git a/package/src/components/ChannelPreview/ChannelPreviewMutedStatus.tsx b/package/src/components/ChannelPreview/ChannelPreviewMutedStatus.tsx index d8bccb8563..0188dca1d3 100644 --- a/package/src/components/ChannelPreview/ChannelPreviewMutedStatus.tsx +++ b/package/src/components/ChannelPreview/ChannelPreviewMutedStatus.tsx @@ -1,7 +1,9 @@ import React from 'react'; +import { useA11yLabel } from '../../a11y/hooks/useA11yLabel'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { Mute } from '../../icons'; +import { CompositeAccessibilityProbe } from '../Accessibility/CompositeAccessibilityProbe'; /** * This UI component displays an avatar for a particular channel. @@ -13,6 +15,11 @@ export const ChannelPreviewMutedStatus = () => { semantics, }, } = useTheme(); + const accessibilityLabel = useA11yLabel('a11y/Muted'); - return ; + return ( + + + + ); }; diff --git a/package/src/components/ChannelPreview/ChannelPreviewStatus.tsx b/package/src/components/ChannelPreview/ChannelPreviewStatus.tsx index 3d3c875ec5..e17d835b1e 100644 --- a/package/src/components/ChannelPreview/ChannelPreviewStatus.tsx +++ b/package/src/components/ChannelPreview/ChannelPreviewStatus.tsx @@ -4,6 +4,7 @@ import { StyleSheet, Text } from 'react-native'; import type { ChannelPreviewProps } from './ChannelPreview'; import type { ChannelPreviewViewPropsWithContext } from './ChannelPreviewView'; +import { useA11yLabel } from '../../a11y/hooks/useA11yLabel'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useTranslationContext } from '../../contexts/translationContext/TranslationContext'; @@ -35,11 +36,16 @@ export const ChannelPreviewStatus = (props: ChannelPreviewStatusProps) => { [created_at, t, tDateTimeParser], ); + const visibleDate = + formatLatestMessageDate && latestMessageDate + ? formatLatestMessageDate(latestMessageDate).toString() + : formattedDate; + const labelParams = useMemo(() => ({ date: visibleDate ?? '' }), [visibleDate]); + const accessibilityLabel = useA11yLabel('a11y/Last message {{date}}', labelParams); + return ( - - {formatLatestMessageDate && latestMessageDate - ? formatLatestMessageDate(latestMessageDate).toString() - : formattedDate} + + {visibleDate} ); }; diff --git a/package/src/components/ChannelPreview/ChannelPreviewUnreadCount.tsx b/package/src/components/ChannelPreview/ChannelPreviewUnreadCount.tsx index e44ecd302d..34a392fdef 100644 --- a/package/src/components/ChannelPreview/ChannelPreviewUnreadCount.tsx +++ b/package/src/components/ChannelPreview/ChannelPreviewUnreadCount.tsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { ChannelPreviewProps } from './ChannelPreview'; +import { useA11yLabel } from '../../a11y/hooks/useA11yLabel'; import type { ChannelsContextValue } from '../../contexts/channelsContext/ChannelsContext'; import { BadgeNotification } from '../ui/Badge'; @@ -15,12 +16,15 @@ export type ChannelPreviewUnreadCountProps = Pick { const { maxUnreadCount, unread } = props; + const labelParams = useMemo(() => ({ count: unread ?? 0 }), [unread]); + const accessibilityLabel = useA11yLabel('a11y/{{count}} unread messages', labelParams); if (!unread) { return null; } return ( maxUnreadCount ? maxUnreadCount : unread} size='sm' type='primary' diff --git a/package/src/components/ChannelPreview/ChannelPreviewView.tsx b/package/src/components/ChannelPreview/ChannelPreviewView.tsx index f57c2e066b..d8922068a6 100644 --- a/package/src/components/ChannelPreview/ChannelPreviewView.tsx +++ b/package/src/components/ChannelPreview/ChannelPreviewView.tsx @@ -5,6 +5,7 @@ import type { ChannelPreviewProps } from './ChannelPreview'; import type { LastMessageType } from './hooks/useChannelPreviewData'; +import { useA11yLabel } from '../../a11y/hooks/useA11yLabel'; import { ChannelsContextValue, useChannelsContext, @@ -70,6 +71,7 @@ const ChannelPreviewViewWithContext = (props: ChannelPreviewViewPropsWithContext } = useTheme(); const styles = useStyles(); const swipeRegistry = useSwipeRegistryContext(); + const accessibilityHint = useA11yLabel('a11y/Double tap to open'); const onPress = useStableCallback(() => { if (swipeRegistry?.hasOpen()) { @@ -84,6 +86,7 @@ const ChannelPreviewViewWithContext = (props: ChannelPreviewViewPropsWithContext return ( [ styles.container, diff --git a/package/src/components/Message/MessageItemView/MessageContent.tsx b/package/src/components/Message/MessageItemView/MessageContent.tsx index 4ce983a7d0..cded1cabbb 100644 --- a/package/src/components/Message/MessageItemView/MessageContent.tsx +++ b/package/src/components/Message/MessageItemView/MessageContent.tsx @@ -318,10 +318,21 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => { ); + // Drop the Pressable's single-focus-stop behavior when the message contains + // interactive children (poll options, attachment cells, the quoted-reply + // navigator, shared location). Without this, VO/TalkBack subsume those + // children into the row's one announcement and they can't be activated. + const hasInteractiveContent = !!( + message.poll_id || + message.quoted_message || + message.attachments?.length || + message.shared_location + ); + return ( { if (onLongPress) { diff --git a/package/src/components/Message/MessageItemView/MessageFooter.tsx b/package/src/components/Message/MessageItemView/MessageFooter.tsx index c9b4653aaa..2335b2bd8a 100644 --- a/package/src/components/Message/MessageItemView/MessageFooter.tsx +++ b/package/src/components/Message/MessageItemView/MessageFooter.tsx @@ -69,7 +69,12 @@ const MessageFooterWithContext = (props: MessageFooterPropsWithContext) => { const isEdited = isEditedMessage(message) && !isAIGenerated; return ( - + {Object.keys(members).length > 2 && alignment === 'left' && message.user?.name ? ( {message.user.name} diff --git a/package/src/components/Message/MessageItemView/MessageStatus.tsx b/package/src/components/Message/MessageItemView/MessageStatus.tsx index 5046ac7ba7..fffd49472c 100644 --- a/package/src/components/Message/MessageItemView/MessageStatus.tsx +++ b/package/src/components/Message/MessageItemView/MessageStatus.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import { StyleSheet, View } from 'react-native'; +import { useA11yLabel } from '../../../a11y/hooks/useA11yLabel'; import { useChannelContext } from '../../../contexts/channelContext/ChannelContext'; import { MessageContextValue, @@ -12,6 +13,7 @@ import { CheckAll } from '../../../icons/checks'; import { Time } from '../../../icons/clock'; import { primitives } from '../../../theme'; import { MessageStatusTypes } from '../../../utils/utils'; +import { HiddenA11yText } from '../../Accessibility/HiddenA11yText'; import { useShouldUseOverlayStyles } from '../hooks/useShouldUseOverlayStyles'; export type MessageStatusPropsWithContext = Pick< @@ -32,10 +34,6 @@ const MessageStatusWithContext = (props: MessageStatusPropsWithContext) => { }, } = useTheme(); - if (message.status === MessageStatusTypes.FAILED || message.type === 'error') { - return null; - } - const hasReadByGreaterThanOne = typeof readBy === 'number' && readBy > 1; // Variables to determine the status of the message @@ -48,42 +46,41 @@ const MessageStatusWithContext = (props: MessageStatusPropsWithContext) => { !read && message.type !== 'ephemeral'; + const accessibilityLabel = useA11yLabel( + read + ? 'a11y/Read' + : delivered + ? 'a11y/Delivered' + : sending + ? 'a11y/Sending' + : sent + ? 'a11y/Sent' + : '', + ); + + if (message.status === MessageStatusTypes.FAILED || message.type === 'error') { + return null; + } + return ( - - {read ? ( - - ) : delivered ? ( - - ) : sending ? ( - + <> + + + {read ? ( + + ) : delivered ? ( + + ) : sending ? ( + + ); }; diff --git a/package/src/components/Message/MessageItemView/ReactionList/ReactionListClustered.tsx b/package/src/components/Message/MessageItemView/ReactionList/ReactionListClustered.tsx index 6115b2e47f..3203f2fe8d 100644 --- a/package/src/components/Message/MessageItemView/ReactionList/ReactionListClustered.tsx +++ b/package/src/components/Message/MessageItemView/ReactionList/ReactionListClustered.tsx @@ -1,8 +1,9 @@ -import React, { useMemo } from 'react'; +import React, { Fragment, useMemo } from 'react'; import { StyleProp, StyleSheet, Text, ViewStyle } from 'react-native'; import { ReactionListItemWrapper } from './ReactionListItemWrapper'; +import { useA11yLabel } from '../../../../a11y/hooks/useA11yLabel'; import { MessageContextValue, useMessageContext, @@ -19,6 +20,7 @@ import type { IconProps } from '../../../../icons/utils/base'; import { primitives } from '../../../../theme'; import type { ReactionData } from '../../../../utils/utils'; +import { HiddenA11yText } from '../../../Accessibility/HiddenA11yText'; type Props = Pick & { size: number; @@ -65,6 +67,8 @@ export const ReactionListClusteredWithContext = (props: ReactionListClusteredPro }, } = useTheme(); const styles = useStyles(); + const accessibilityHint = useA11yLabel('a11y/Double tap to view reactions'); + const youReacted = useA11yLabel('a11y/you reacted'); const supportedReactionTypes = supportedReactions?.map( (supportedReaction) => supportedReaction.type, ); @@ -72,6 +76,9 @@ export const ReactionListClusteredWithContext = (props: ReactionListClusteredPro const moreReactionsCount = reactionsCount - 4; const reactionsCountText = moreReactionsCount < 99 ? moreReactionsCount : `+${moreReactionsCount}`; + const moreReactionsA11yText = useA11yLabel('a11y/and {{count}} more reactions', { + count: moreReactionsCount, + }); const hasSupportedReactions = reactions?.some((reaction) => supportedReactionTypes?.includes(reaction.type), @@ -83,6 +90,8 @@ export const ReactionListClusteredWithContext = (props: ReactionListClusteredPro return ( { if (onPress) { @@ -111,18 +120,23 @@ export const ReactionListClusteredWithContext = (props: ReactionListClusteredPro } }} style={containerStyle} - accessibilityLabel={accessibilityLabel} > {reactions?.slice(0, 4).map((reaction) => ( - + + + {reaction.own ? : null} + ))} - {reactionsCount > 4 ? {reactionsCountText} : null} + {reactionsCount > 4 ? ( + + {reactionsCountText} + + ) : null} ); }; diff --git a/package/src/components/Message/MessageItemView/ReactionList/ReactionListItem.tsx b/package/src/components/Message/MessageItemView/ReactionList/ReactionListItem.tsx index 5f45a5a20a..e91f7e9635 100644 --- a/package/src/components/Message/MessageItemView/ReactionList/ReactionListItem.tsx +++ b/package/src/components/Message/MessageItemView/ReactionList/ReactionListItem.tsx @@ -3,6 +3,7 @@ import { StyleSheet, Text } from 'react-native'; import { ReactionListItemWrapper } from './ReactionListItemWrapper'; +import { useA11yLabel } from '../../../../a11y/hooks/useA11yLabel'; import { MessageContextValue } from '../../../../contexts/messageContext/MessageContext'; import { MessagesContextValue } from '../../../../contexts/messagesContext/MessagesContext'; import { useTheme } from '../../../../contexts/themeContext/ThemeContext'; @@ -13,6 +14,7 @@ import type { IconProps } from '../../../../icons/utils/base'; import { primitives } from '../../../../theme'; import type { ReactionData } from '../../../../utils/utils'; +import { HiddenA11yText } from '../../../Accessibility/HiddenA11yText'; import { ReactionSummary } from '../../hooks/useProcessReactions'; type Props = Pick & { @@ -66,12 +68,13 @@ export const ReactionListItem = (props: ReactionListItemProps) => { }, } = useTheme(); const styles = useStyles(); + const youReacted = useA11yLabel('a11y/you reacted'); return ( { if (onLongPress) { onLongPress({ @@ -121,6 +124,7 @@ export const ReactionListItem = (props: ReactionListItemProps) => { type={reaction.type} /> {showCount ? {reaction.count} : null} + {selected ? : null} ); }; diff --git a/package/src/components/Message/MessageItemView/__tests__/MessageStatus.test.tsx b/package/src/components/Message/MessageItemView/__tests__/MessageStatus.test.tsx index e8ea53fab7..acf705c17f 100644 --- a/package/src/components/Message/MessageItemView/__tests__/MessageStatus.test.tsx +++ b/package/src/components/Message/MessageItemView/__tests__/MessageStatus.test.tsx @@ -5,6 +5,7 @@ import type { Channel as ChannelType, StreamChat } from 'stream-chat'; import { Channel } from '../../..'; import { ChannelsStateProvider } from '../../../../contexts/channelsStateContext/ChannelsStateContext'; +import { OverlayProvider } from '../../../../contexts/overlayContext/OverlayProvider'; import { getOrCreateChannelApi } from '../../../../mock-builders/api/getOrCreateChannel'; import { useMockedApis } from '../../../../mock-builders/api/useMockedApis'; import { generateChannelResponse } from '../../../../mock-builders/generator/channel'; @@ -54,13 +55,15 @@ describe('MessageStatus', () => { channelProps?: Partial>, ) => render( - - - - - - - , + + + + + + + + + , ); // NOTE: Original source had `it.each('string', async () => { ... })` which was a diff --git a/package/src/components/Message/MessageItemView/__tests__/ReactionListBottom.test.tsx b/package/src/components/Message/MessageItemView/__tests__/ReactionListBottom.test.tsx index 6ff6d39dae..1ee46bdd8c 100644 --- a/package/src/components/Message/MessageItemView/__tests__/ReactionListBottom.test.tsx +++ b/package/src/components/Message/MessageItemView/__tests__/ReactionListBottom.test.tsx @@ -168,7 +168,7 @@ describe('ReactionListBottom', () => { { reactionListPosition: 'bottom', reactionListType: 'segmented' }, ); - const reactionListBottomItem = screen.getByLabelText('Reaction List Item'); + const reactionListBottomItem = screen.getByTestId('reaction-list-item'); fireEvent(reactionListBottomItem, 'onPress'); diff --git a/package/src/components/Message/hooks/useMessageActionHandlers.ts b/package/src/components/Message/hooks/useMessageActionHandlers.ts index 5bd9e65a95..8ed432d9c7 100644 --- a/package/src/components/Message/hooks/useMessageActionHandlers.ts +++ b/package/src/components/Message/hooks/useMessageActionHandlers.ts @@ -5,10 +5,12 @@ import { UserResponse } from 'stream-chat'; import { useUserMuteActive } from './useUserMuteActive'; +import { useScreenReaderEnabled } from '../../../a11y/hooks/useScreenReaderEnabled'; import type { ChannelContextValue } from '../../../contexts/channelContext/ChannelContext'; import type { ChatContextValue } from '../../../contexts/chatContext/ChatContext'; import { MessageComposerAPIContextValue } from '../../../contexts/messageComposerContext/MessageComposerAPIContext'; import type { MessageContextValue } from '../../../contexts/messageContext/MessageContext'; +import { useMessageInputContext } from '../../../contexts/messageInputContext/MessageInputContext'; import type { MessagesContextValue } from '../../../contexts/messagesContext/MessagesContext'; import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext'; @@ -67,6 +69,8 @@ export const useMessageActionHandlers = ({ Pick) => { const { t } = useTranslationContext(); const { addNotification } = useNotificationApi(); + const { inputBoxRef } = useMessageInputContext(); + const screenReaderEnabled = useScreenReaderEnabled(); const handleResendMessage = useStableCallback(() => retrySendMessage(message)); const translatedMessage = useTranslatedMessage(message); @@ -74,6 +78,9 @@ export const useMessageActionHandlers = ({ const handleQuotedReplyMessage = useStableCallback(() => { setQuotedMessage(message); + if (screenReaderEnabled) { + inputBoxRef.current?.focus(); + } }); const handleCopyMessage = useStableCallback(() => { diff --git a/package/src/components/Poll/Poll.tsx b/package/src/components/Poll/Poll.tsx index 8c839cfa81..0716485fbc 100644 --- a/package/src/components/Poll/Poll.tsx +++ b/package/src/components/Poll/Poll.tsx @@ -6,18 +6,14 @@ import { PollOption as PollOptionClass } from 'stream-chat'; import { PollOption, ShowAllOptionsButton } from './components'; import { PollUIStateProvider } from './contexts/PollUIStateContext'; -import { usePollAccessibilityActions } from './hooks/usePollAccessibilityActions'; -import { usePollAccessibilityLabel } from './hooks/usePollAccessibilityLabel'; import { usePollState } from './hooks/usePollState'; -import { useA11yLabel } from '../../a11y/hooks/useA11yLabel'; import { PollContextProvider, PollContextValue, useTheme, useTranslationContext, } from '../../contexts'; -import { useAccessibilityContext } from '../../contexts/accessibilityContext/AccessibilityContext'; import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { primitives } from '../../theme'; @@ -66,10 +62,6 @@ export const PollContent = () => { const styles = useStyles(); const { PollButtons: PollButtonsComponent, PollHeader: PollHeaderComponent } = useComponentsContext(); - const { enabled: a11yEnabled } = useAccessibilityContext(); - const accessibilityHint = useA11yLabel('a11y/Double tap and hold to activate contextual menu'); - const accessibilityLabel = usePollAccessibilityLabel(); - const { accessibilityActions, onAccessibilityAction } = usePollAccessibilityActions(); const { theme: { @@ -79,24 +71,8 @@ export const PollContent = () => { }, } = useTheme(); - // NOTE: Android custom accessibilityActions are broken in RN < 0.83.2 — - // see facebook/react-native#47268, fixed by PR #52724. On affected versions - // the actions menu surfaces only a subset of the list and dispatch - // announces "Action not supported". iOS works correctly on all versions. - // Once the SDK's minimum RN reaches 0.83.2, wrap the descendants below in - // so Android - // TalkBack groups them under the composite rather than exposing each - // interactive child as a separate focus stop. return ( - + {options?.slice(0, defaultPollOptionCount)?.map((option: PollOptionClass) => ( diff --git a/package/src/components/Poll/hooks/__tests__/usePollAccessibilityActions.test.tsx b/package/src/components/Poll/hooks/__tests__/usePollAccessibilityActions.test.tsx deleted file mode 100644 index fabff2cf88..0000000000 --- a/package/src/components/Poll/hooks/__tests__/usePollAccessibilityActions.test.tsx +++ /dev/null @@ -1,358 +0,0 @@ -import React from 'react'; - -import type { AccessibilityActionEvent } from 'react-native'; - -import { act, renderHook } from '@testing-library/react-native'; - -import { AccessibilityProvider } from '../../../../contexts/accessibilityContext/AccessibilityContext'; -import { TranslationProvider } from '../../../../contexts/translationContext/TranslationContext'; -import { usePollAccessibilityActions } from '../usePollAccessibilityActions'; - -const mockOpenAddComment = jest.fn(); -const mockOpenAllComments = jest.fn(); -const mockOpenAllOptions = jest.fn(); -const mockOpenSuggestOption = jest.fn(); -const mockOpenViewResults = jest.fn(); -const mockEndVote = jest.fn(); -const mockToggleVote = jest.fn(); - -jest.mock('../../contexts/PollUIStateContext', () => ({ - usePollUIStateContext: () => ({ - openAddComment: mockOpenAddComment, - openAllComments: mockOpenAllComments, - openAllOptions: mockOpenAllOptions, - openSuggestOption: mockOpenSuggestOption, - openViewResults: mockOpenViewResults, - }), -})); - -jest.mock('../usePollStateStore', () => ({ - usePollStateStore: (selector: (state: unknown) => unknown) => selector(mockPollState), -})); - -jest.mock('../useEndVote', () => ({ - useEndVote: () => mockEndVote, -})); - -jest.mock('../usePollVoteToggle', () => ({ - usePollVoteToggle: () => mockToggleVote, -})); - -const mockChatContext = { client: { userID: 'me' } }; -const mockOwnCapabilities = { castPollVote: true }; - -jest.mock('../../../../contexts', () => { - const actual = jest.requireActual('../../../../contexts'); - return { - ...actual, - useChatContext: () => mockChatContext, - useOwnCapabilitiesContext: () => mockOwnCapabilities, - }; -}); - -let mockPollState: Record = {}; - -const setPollState = (state: Record) => { - mockPollState = state; -}; - -const setCastPollVote = (allowed: boolean) => { - mockOwnCapabilities.castPollVote = allowed; -}; - -const setUserID = (id: string) => { - mockChatContext.client.userID = id; -}; - -const t = (key: string, vars?: Record) => { - if (!vars) return key; - if (key === 'a11y/Vote on {{option}}') return `Vote on ${vars.option}`; - return key; -}; - -const wrapper = - (enabled: boolean) => - ({ children }: { children: React.ReactNode }) => ( - - null, - } as never - } - > - {children} - - - ); - -const buildOption = (id: string, text: string) => ({ id, text }); - -const fireAction = ( - handler: ((event: AccessibilityActionEvent) => void) | undefined, - actionName: string, -) => { - handler?.({ nativeEvent: { actionName } } as AccessibilityActionEvent); -}; - -beforeEach(() => { - mockOpenAddComment.mockClear(); - mockOpenAllComments.mockClear(); - mockOpenAllOptions.mockClear(); - mockOpenSuggestOption.mockClear(); - mockOpenViewResults.mockClear(); - mockEndVote.mockClear(); - mockToggleVote.mockClear(); - setCastPollVote(true); - setUserID('me'); -}); - -describe('usePollAccessibilityActions', () => { - it('returns undefined when accessibility is disabled', () => { - setPollState({ - allow_answers: true, - allow_user_suggested_options: true, - created_by: { id: 'me' }, - is_closed: false, - options: [buildOption('o1', 'A')], - }); - - const { result } = renderHook(() => usePollAccessibilityActions(), { - wrapper: wrapper(false), - }); - - expect(result.current.accessibilityActions).toBeUndefined(); - expect(result.current.onAccessibilityAction).toBeUndefined(); - }); - - it('every action uses the same human label for name and label', () => { - setPollState({ - allow_answers: true, - allow_user_suggested_options: true, - created_by: { id: 'me' }, - is_closed: false, - options: [buildOption('o1', 'Pizza'), buildOption('o2', 'Pasta')], - }); - - const { result } = renderHook(() => usePollAccessibilityActions(), { - wrapper: wrapper(true), - }); - - const actions = result.current.accessibilityActions; - expect(actions).toBeDefined(); - for (const action of actions ?? []) { - expect(action.name).toBe(action.label); - } - }); - - it('exposes only View Results for an ended poll', () => { - setPollState({ - allow_answers: true, - allow_user_suggested_options: true, - created_by: { id: 'me' }, - is_closed: true, - options: [buildOption('o1', 'A'), buildOption('o2', 'B')], - }); - - const { result } = renderHook(() => usePollAccessibilityActions(), { - wrapper: wrapper(true), - }); - - const labels = result.current.accessibilityActions?.map((a) => a.label); - expect(labels).toEqual(['View Results']); - }); - - it('lists vote actions with the option text, plus End vote / Add comment / Suggest option for creator', () => { - setPollState({ - allow_answers: true, - allow_user_suggested_options: true, - created_by: { id: 'me' }, - is_closed: false, - options: [buildOption('o1', 'Pizza'), buildOption('o2', 'Pasta')], - }); - - const { result } = renderHook(() => usePollAccessibilityActions(), { - wrapper: wrapper(true), - }); - - const labels = result.current.accessibilityActions?.map((a) => a.label); - expect(labels).toEqual([ - 'View Results', - 'Vote on Pizza', - 'Vote on Pasta', - 'a11y/End vote', - 'Add a comment', - 'Suggest an option', - ]); - }); - - it('omits End vote when the current user is not the creator', () => { - setUserID('someone-else'); - setPollState({ - allow_answers: false, - allow_user_suggested_options: false, - created_by: { id: 'me' }, - is_closed: false, - options: [buildOption('o1', 'Pizza')], - }); - - const { result } = renderHook(() => usePollAccessibilityActions(), { - wrapper: wrapper(true), - }); - - const labels = result.current.accessibilityActions?.map((a) => a.label); - expect(labels).toEqual(['View Results', 'Vote on Pizza']); - }); - - it('omits vote actions when the user lacks castPollVote capability', () => { - setCastPollVote(false); - setPollState({ - allow_answers: true, - allow_user_suggested_options: false, - created_by: { id: 'somebody' }, - is_closed: false, - options: [buildOption('o1', 'Pizza')], - }); - - const { result } = renderHook(() => usePollAccessibilityActions(), { - wrapper: wrapper(true), - }); - - const labels = result.current.accessibilityActions?.map((a) => a.label); - expect(labels?.some((l) => l?.startsWith('Vote on'))).toBe(false); - }); - - it('exposes "View N comments" when the poll has answers', () => { - setPollState({ - allow_answers: false, - allow_user_suggested_options: false, - answers_count: 4, - created_by: { id: 'somebody' }, - is_closed: true, - options: [buildOption('o1', 'A')], - }); - - const { result } = renderHook(() => usePollAccessibilityActions(), { - wrapper: wrapper(true), - }); - - const labels = result.current.accessibilityActions?.map((a) => a.label); - expect(labels).toContain('View {{count}} comments'); - }); - - it('omits "View N comments" when there are no answers', () => { - setPollState({ - allow_answers: false, - allow_user_suggested_options: false, - answers_count: 0, - created_by: { id: 'somebody' }, - is_closed: true, - options: [buildOption('o1', 'A')], - }); - - const { result } = renderHook(() => usePollAccessibilityActions(), { - wrapper: wrapper(true), - }); - - const labels = result.current.accessibilityActions?.map((a) => a.label); - expect(labels?.some((l) => l?.includes('comments'))).toBe(false); - }); - - it('exposes Show all options when options exceed the visible cap', () => { - const manyOptions = Array.from({ length: 12 }, (_, i) => buildOption(`o${i}`, `Option ${i}`)); - setPollState({ - allow_answers: false, - allow_user_suggested_options: false, - created_by: { id: 'somebody' }, - is_closed: true, - options: manyOptions, - }); - - const { result } = renderHook(() => usePollAccessibilityActions(), { - wrapper: wrapper(true), - }); - - const labels = result.current.accessibilityActions?.map((a) => a.label); - expect(labels).toContain('a11y/Show all options'); - }); - - it('routes each action to the right side effect', () => { - setPollState({ - allow_answers: true, - allow_user_suggested_options: true, - created_by: { id: 'me' }, - is_closed: false, - options: [buildOption('o1', 'Pizza'), buildOption('o2', 'Pasta')], - }); - - const { result } = renderHook(() => usePollAccessibilityActions(), { - wrapper: wrapper(true), - }); - - act(() => { - fireAction(result.current.onAccessibilityAction, 'View Results'); - }); - expect(mockOpenViewResults).toHaveBeenCalledTimes(1); - - act(() => { - fireAction(result.current.onAccessibilityAction, 'a11y/End vote'); - }); - expect(mockEndVote).toHaveBeenCalledTimes(1); - - act(() => { - fireAction(result.current.onAccessibilityAction, 'Add a comment'); - }); - expect(mockOpenAddComment).toHaveBeenCalledTimes(1); - - act(() => { - fireAction(result.current.onAccessibilityAction, 'Suggest an option'); - }); - expect(mockOpenSuggestOption).toHaveBeenCalledTimes(1); - - act(() => { - fireAction(result.current.onAccessibilityAction, 'Vote on Pasta'); - }); - expect(mockToggleVote).toHaveBeenCalledWith('o2'); - }); - - it('routes the "View N comments" action to openAllComments', () => { - setPollState({ - allow_answers: false, - allow_user_suggested_options: false, - answers_count: 7, - created_by: { id: 'somebody' }, - is_closed: true, - options: [buildOption('o1', 'A')], - }); - - const { result } = renderHook(() => usePollAccessibilityActions(), { - wrapper: wrapper(true), - }); - - act(() => { - fireAction(result.current.onAccessibilityAction, 'View {{count}} comments'); - }); - expect(mockOpenAllComments).toHaveBeenCalledTimes(1); - }); - - it('ignores unknown action names', () => { - setPollState({ - allow_answers: true, - allow_user_suggested_options: true, - created_by: { id: 'me' }, - is_closed: false, - options: [buildOption('o1', 'Pizza')], - }); - - const { result } = renderHook(() => usePollAccessibilityActions(), { - wrapper: wrapper(true), - }); - - act(() => { - fireAction(result.current.onAccessibilityAction, 'streamPollVoteOption_o1'); - }); - expect(mockToggleVote).not.toHaveBeenCalled(); - expect(mockOpenViewResults).not.toHaveBeenCalled(); - }); -}); diff --git a/package/src/components/Poll/hooks/__tests__/usePollAccessibilityLabel.test.tsx b/package/src/components/Poll/hooks/__tests__/usePollAccessibilityLabel.test.tsx deleted file mode 100644 index 3a0ec50c2a..0000000000 --- a/package/src/components/Poll/hooks/__tests__/usePollAccessibilityLabel.test.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import React from 'react'; - -import { renderHook } from '@testing-library/react-native'; - -import { AccessibilityProvider } from '../../../../contexts/accessibilityContext/AccessibilityContext'; -import { TranslationProvider } from '../../../../contexts/translationContext/TranslationContext'; -import { usePollAccessibilityLabel } from '../usePollAccessibilityLabel'; - -jest.mock('../usePollStateStore', () => ({ - usePollStateStore: (selector: (state: unknown) => unknown) => selector(mockPollState), -})); - -let mockPollState: Record = {}; - -const setPollState = (state: Record) => { - mockPollState = state; -}; - -const t = (key: string, vars?: Record) => { - if (!vars) return key; - if (key === '{{count}} votes') return `${vars.count} votes`; - if (key === 'Select up to {{count}}') return `Select up to ${vars.count}`; - if (key === '+{{count}} More Options') return `+${vars.count} More Options`; - return key; -}; - -const wrapper = - (enabled: boolean) => - ({ children }: { children: React.ReactNode }) => ( - - null, - } as never - } - > - {children} - - - ); - -const buildOption = (id: string, text: string) => ({ id, text }); - -describe('usePollAccessibilityLabel', () => { - it('returns undefined when accessibility is disabled', () => { - setPollState({ - enforce_unique_vote: false, - is_closed: true, - max_votes_allowed: 0, - name: 'Lunch?', - options: [buildOption('o1', 'Pizza')], - vote_counts_by_option: { o1: 3 }, - }); - - const { result } = renderHook(() => usePollAccessibilityLabel(), { - wrapper: wrapper(false), - }); - - expect(result.current).toBeUndefined(); - }); - - it('builds composite label for an ended poll', () => { - setPollState({ - enforce_unique_vote: false, - is_closed: true, - max_votes_allowed: 0, - name: 'Test', - options: [buildOption('o1', 'Option 1'), buildOption('o2', 'Option 2')], - vote_counts_by_option: { o1: 0, o2: 0 }, - }); - - const { result } = renderHook(() => usePollAccessibilityLabel(), { - wrapper: wrapper(true), - }); - - expect(result.current).toBe( - 'Test, Poll has ended, Option 1: 0 votes, Option 2: 0 votes, a11y/Activate to view results', - ); - }); - - it('uses "Select one" for an open enforce-unique-vote poll', () => { - setPollState({ - enforce_unique_vote: true, - is_closed: false, - max_votes_allowed: 0, - name: 'Pick a venue', - options: [buildOption('o1', 'Cafe')], - vote_counts_by_option: { o1: 2 }, - }); - - const { result } = renderHook(() => usePollAccessibilityLabel(), { - wrapper: wrapper(true), - }); - - expect(result.current).toBe( - 'Pick a venue, Select one, Cafe: 2 votes, a11y/Activate to view results', - ); - }); - - it('uses "Select up to N" when maxVotesAllowed is set', () => { - setPollState({ - enforce_unique_vote: false, - is_closed: false, - max_votes_allowed: 3, - name: 'Top picks', - options: [buildOption('o1', 'A')], - vote_counts_by_option: { o1: 1 }, - }); - - const { result } = renderHook(() => usePollAccessibilityLabel(), { - wrapper: wrapper(true), - }); - - expect(result.current).toBe( - 'Top picks, Select up to 3, A: 1 votes, a11y/Activate to view results', - ); - }); - - it('appends overflow hint when options exceed the visible cap', () => { - const manyOptions = Array.from({ length: 12 }, (_, i) => buildOption(`o${i}`, `Option ${i}`)); - const counts = Object.fromEntries(manyOptions.map((o) => [o.id, 0])); - - setPollState({ - enforce_unique_vote: false, - is_closed: false, - max_votes_allowed: 0, - name: 'Big poll', - options: manyOptions, - vote_counts_by_option: counts, - }); - - const { result } = renderHook(() => usePollAccessibilityLabel(), { - wrapper: wrapper(true), - }); - - expect(result.current).toContain('+7 More Options'); - expect(result.current).toContain('Option 0: 0 votes'); - expect(result.current).not.toContain('Option 5:'); - }); -}); diff --git a/package/src/components/Poll/hooks/usePollAccessibilityActions.ts b/package/src/components/Poll/hooks/usePollAccessibilityActions.ts deleted file mode 100644 index 86d85735d7..0000000000 --- a/package/src/components/Poll/hooks/usePollAccessibilityActions.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { useMemo } from 'react'; - -import type { AccessibilityActionEvent, AccessibilityProps } from 'react-native'; - -import { PollOption, PollState, UserResponse } from 'stream-chat'; - -import { useEndVote } from './useEndVote'; - -import { usePollStateStore } from './usePollStateStore'; - -import { usePollVoteToggle } from './usePollVoteToggle'; - -import { - useChatContext, - useOwnCapabilitiesContext, - useTranslationContext, -} from '../../../contexts'; -import { useAccessibilityContext } from '../../../contexts/accessibilityContext/AccessibilityContext'; -import { useStableCallback } from '../../../hooks'; -import { defaultPollOptionCount } from '../../../utils/constants'; -import { usePollUIStateContext } from '../contexts/PollUIStateContext'; - -type AccessibilityAction = NonNullable[number]; -type OnAccessibilityAction = NonNullable; - -type PollA11yActionsSelectorResult = { - allowAnswers: boolean | undefined; - allowUserSuggestedOptions: boolean | undefined; - answersCount: number; - createdBy: UserResponse | null; - isClosed: boolean | undefined; - options: PollOption[]; -}; - -const a11yActionsSelector = (state: PollState): PollA11yActionsSelectorResult => ({ - allowAnswers: state.allow_answers, - allowUserSuggestedOptions: state.allow_user_suggested_options, - answersCount: state.answers_count, - createdBy: state.created_by, - isClosed: state.is_closed, - options: state.options, -}); - -export type UsePollAccessibilityActionsResult = { - accessibilityActions: readonly AccessibilityAction[] | undefined; - onAccessibilityAction: OnAccessibilityAction | undefined; -}; - -type ActionKind = - | { type: 'addComment' } - | { type: 'endVote' } - | { type: 'showAllComments' } - | { type: 'showAllOptions' } - | { type: 'suggestOption' } - | { type: 'viewResults' } - | { type: 'vote'; optionId: string }; - -/** - * Returns the `accessibilityActions` array and `onAccessibilityAction` handler - * for the poll composite container. Action set is gated by poll state + - * capabilities so each rotor entry corresponds to an interaction the user is - * actually allowed to perform. Returns `undefined`s when a11y is disabled. - * - * NOTE: We set both `name` and `label` to the same human-readable string on - * every action. iOS Fabric (new architecture, on by default in RN 0.81+) uses - * `accessibilityAction.name` as the string VoiceOver reads — `label` is - * ignored on that path (RCTViewComponentView.mm). iOS legacy (Paper) and - * Android both read `label`. Using the same value for both fields means the - * announcement is human-readable on every platform/architecture. Dispatch - * uses the action name as the lookup key into an internal kind map, so the - * raw strings never need to be exposed to consumers. - */ -export const usePollAccessibilityActions = (): UsePollAccessibilityActionsResult => { - const { enabled } = useAccessibilityContext(); - const { t } = useTranslationContext(); - const { client } = useChatContext(); - const { castPollVote } = useOwnCapabilitiesContext(); - const { allowAnswers, allowUserSuggestedOptions, answersCount, createdBy, isClosed, options } = - usePollStateStore(a11yActionsSelector); - const { openAddComment, openAllComments, openAllOptions, openSuggestOption, openViewResults } = - usePollUIStateContext(); - const toggleVote = usePollVoteToggle(); - const endVote = useEndVote(); - - const canVote = !isClosed && !!castPollVote; - const canEnd = !isClosed && createdBy?.id === client.userID; - const canComment = !isClosed && !!allowAnswers; - const canSuggest = !isClosed && !!allowUserSuggestedOptions; - const hasMoreOptions = !!options && options.length > defaultPollOptionCount; - const hasComments = answersCount > 0; - - const { accessibilityActions, actionKindByName } = useMemo<{ - accessibilityActions: readonly AccessibilityAction[] | undefined; - actionKindByName: Map | undefined; - }>(() => { - if (!enabled) { - return { accessibilityActions: undefined, actionKindByName: undefined }; - } - - const actions: AccessibilityAction[] = []; - const kindByName = new Map(); - - const push = (name: string, kind: ActionKind) => { - actions.push({ label: name, name }); - kindByName.set(name, kind); - }; - - push(t('View Results'), { type: 'viewResults' }); - - if (canVote && options) { - for (const option of options.slice(0, defaultPollOptionCount)) { - push(t('a11y/Vote on {{option}}', { option: option.text }), { - optionId: option.id, - type: 'vote', - }); - } - } - - if (hasMoreOptions) { - push(t('a11y/Show all options'), { type: 'showAllOptions' }); - } - - if (canEnd) { - push(t('a11y/End vote'), { type: 'endVote' }); - } - - if (canComment) { - push(t('Add a comment'), { type: 'addComment' }); - } - - if (canSuggest) { - push(t('Suggest an option'), { type: 'suggestOption' }); - } - - if (hasComments) { - push(t('View {{count}} comments', { count: answersCount }), { type: 'showAllComments' }); - } - - return { accessibilityActions: actions, actionKindByName: kindByName }; - }, [ - answersCount, - canComment, - canEnd, - canSuggest, - canVote, - enabled, - hasComments, - hasMoreOptions, - options, - t, - ]); - - const onAccessibilityAction = useStableCallback((event: AccessibilityActionEvent) => { - const kind = actionKindByName?.get(event.nativeEvent.actionName); - if (!kind) return; - - switch (kind.type) { - case 'viewResults': - openViewResults(); - return; - case 'showAllOptions': - openAllOptions(); - return; - case 'endVote': - void endVote(); - return; - case 'addComment': - openAddComment(); - return; - case 'suggestOption': - openSuggestOption(); - return; - case 'showAllComments': - openAllComments(); - return; - case 'vote': - void toggleVote(kind.optionId); - return; - default: - return; - } - }); - - return useMemo( - () => ({ - accessibilityActions, - onAccessibilityAction: enabled ? onAccessibilityAction : undefined, - }), - [accessibilityActions, enabled, onAccessibilityAction], - ); -}; diff --git a/package/src/components/Poll/hooks/usePollAccessibilityLabel.ts b/package/src/components/Poll/hooks/usePollAccessibilityLabel.ts deleted file mode 100644 index 5617ae8fd0..0000000000 --- a/package/src/components/Poll/hooks/usePollAccessibilityLabel.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { useMemo } from 'react'; - -import { PollOption, PollState } from 'stream-chat'; - -import { usePollStateStore } from './usePollStateStore'; - -import { composeAccessibilityLabel } from '../../../a11y/a11yUtils'; -import { useTranslationContext } from '../../../contexts'; -import { useAccessibilityContext } from '../../../contexts/accessibilityContext/AccessibilityContext'; -import { defaultPollOptionCount } from '../../../utils/constants'; - -type PollA11yLabelSelectorResult = { - enforceUniqueVote: boolean; - isClosed: boolean | undefined; - maxVotesAllowed: number; - name: string; - options: PollOption[]; - voteCountsByOption: Record; -}; - -const a11yLabelSelector = (state: PollState): PollA11yLabelSelectorResult => ({ - enforceUniqueVote: state.enforce_unique_vote, - isClosed: state.is_closed, - maxVotesAllowed: state.max_votes_allowed, - name: state.name, - options: state.options, - voteCountsByOption: state.vote_counts_by_option, -}); - -/** - * Builds the composite accessibility label for a poll bubble: name, status, - * up to `defaultPollOptionCount` options with vote counts, an overflow hint, - * and the primary-tap hint. Returns `undefined` when a11y is disabled so the - * Poll container can leave its `accessibilityLabel` unset. - */ -export const usePollAccessibilityLabel = (): string | undefined => { - const { enabled } = useAccessibilityContext(); - const { t } = useTranslationContext(); - const { enforceUniqueVote, isClosed, maxVotesAllowed, name, options, voteCountsByOption } = - usePollStateStore(a11yLabelSelector); - - return useMemo(() => { - if (!enabled) return undefined; - - let status: string; - if (isClosed) { - status = t('Poll has ended'); - } else if (enforceUniqueVote) { - status = t('Select one'); - } else if (maxVotesAllowed) { - status = t('Select up to {{count}}', { count: maxVotesAllowed }); - } else { - status = t('Select one or more'); - } - - const visibleOptions = options?.slice(0, defaultPollOptionCount) ?? []; - const optionParts = visibleOptions.map((option) => { - const count = voteCountsByOption?.[option.id] ?? 0; - return `${option.text}: ${t('{{count}} votes', { count })}`; - }); - - const overflow = - options && options.length > defaultPollOptionCount - ? t('+{{count}} More Options', { count: options.length - defaultPollOptionCount }) - : null; - - return composeAccessibilityLabel( - name, - status, - ...optionParts, - overflow, - t('a11y/Activate to view results'), - ); - }, [enabled, enforceUniqueVote, isClosed, maxVotesAllowed, name, options, t, voteCountsByOption]); -}; diff --git a/package/src/components/Reply/Reply.tsx b/package/src/components/Reply/Reply.tsx index 819388e542..6578237412 100644 --- a/package/src/components/Reply/Reply.tsx +++ b/package/src/components/Reply/Reply.tsx @@ -20,6 +20,7 @@ import { import { ReplyMessageView } from './ReplyMessageView'; +import { useAnnounceOnShow } from '../../a11y/hooks/useAnnounceOnShow'; import { useChatContext } from '../../contexts/chatContext/ChatContext'; import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { @@ -42,6 +43,8 @@ const messageComposerStateStoreSelector = (state: MessageComposerState) => ({ quotedMessage: state.quotedMessage, }); +const ANNOUNCEMENT_TEXT_MAX_LENGTH = 120; + const RightContent = React.memo( (props: Pick) => { const { ImageComponent, message } = props; @@ -234,6 +237,43 @@ export const MemoizedReply = React.memo(ReplyWithContext, areEqual) as typeof Re export type ReplyProps = Partial & Pick; +/** + * Mounted only when the Reply is rendered as the composer header preview + * (edit/reply). Keeps the translation + announcer subscriptions off the + * per-row in-message quoted-reply render path. + */ +const ReplyComposerAnnouncer = ({ + message, + mode, +}: { + message: ReplyPropsWithContext['quotedMessage']; + mode: ReplyPropsWithContext['mode']; +}) => { + const { t } = useTranslationContext(); + const truncatedText = useMemo(() => { + const raw = message?.text?.trim(); + if (!raw) return undefined; + return raw.length > ANNOUNCEMENT_TEXT_MAX_LENGTH + ? `${raw.slice(0, ANNOUNCEMENT_TEXT_MAX_LENGTH).trimEnd()}…` + : raw; + }, [message?.text]); + const announcement = useMemo(() => { + if (mode === 'edit') { + return truncatedText + ? t('a11y/Editing message: {{text}}', { text: truncatedText }) + : t('a11y/Editing message'); + } + const name = message?.user?.name; + if (!name) return undefined; + return truncatedText + ? t('a11y/Replying to {{user}}: {{text}}', { text: truncatedText, user: name }) + : t('a11y/Replying to {{user}}', { user: name }); + }, [mode, message?.user?.name, truncatedText, t]); + + useAnnounceOnShow(true, announcement); + return null; +}; + export const Reply = (props: ReplyProps) => { const { message: messageFromContext } = useMessageContext(); const { client } = useChatContext(); @@ -251,14 +291,25 @@ export const Reply = (props: ReplyProps) => { const isMyMessage = client.user?.id === quotedMessage?.user?.id; + // Composer header passes `onDismiss`; the in-message quoted-reply renderer + // does not. Only the composer-preview path pays for announcement work. + const isComposerPreview = !!props.onDismiss; + return ( - + <> + {isComposerPreview ? ( + // Edit passes the message via `quotedMessage` prop; reply uses the + // composer-state quoted message we computed locally. + + ) : null} + + ); }; diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap index 593a2c7d0c..9f68c78a50 100644 --- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap +++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap @@ -588,6 +588,8 @@ exports[`Thread should match thread snapshot 1`] = ` { const channelName = (channel.data?.name as string | undefined) ?? channel.cid; - if (channelImage) { - return ( - - ); - } + const memberCount = Object.keys(channel.state.members).length; + const isGroup = !!channel.data?.name || memberCount > 2; + const otherUserName = usersWithoutSelf[0]?.name || usersWithoutSelf[0]?.id; + const labelParams = useMemo( + () => ({ count: memberCount, name: otherUserName ?? '' }), + [memberCount, otherUserName], + ); + const accessibilityLabel = useA11yLabel( + isGroup ? 'a11y/Channel with {{count}} members' : 'a11y/Direct chat with {{name}}', + labelParams, + ); - if (usersWithoutSelf.length > 1) { - return ( - - ); - } else { - return ( - - ); - } + return ( + + {channelImage ? ( + + ) : usersWithoutSelf.length > 1 ? ( + + ) : ( + + )} + + ); }; diff --git a/package/src/components/ui/Badge/BadgeNotification.tsx b/package/src/components/ui/Badge/BadgeNotification.tsx index cc47b5dad7..3c08dc099a 100644 --- a/package/src/components/ui/Badge/BadgeNotification.tsx +++ b/package/src/components/ui/Badge/BadgeNotification.tsx @@ -9,6 +9,11 @@ export type BadgeNotificationProps = { count: number; size: 'sm' | 'xs'; testID?: string; + /** + * Optional accessibility label override. When provided, screen readers + * announce this string instead of the bare count. + */ + accessibilityLabel?: string; }; const sizes = { @@ -34,7 +39,7 @@ const textStyles = { }; export const BadgeNotification = (props: BadgeNotificationProps) => { - const { type = 'primary', count, size = 'sm', testID } = props; + const { accessibilityLabel, type = 'primary', count, size = 'sm', testID } = props; const styles = useStyles(); const { theme: { semantics }, @@ -49,7 +54,11 @@ export const BadgeNotification = (props: BadgeNotificationProps) => { return ( - + {count} diff --git a/package/src/i18n/ar.json b/package/src/i18n/ar.json index 829a76af98..c8441fd0ca 100644 --- a/package/src/i18n/ar.json +++ b/package/src/i18n/ar.json @@ -253,28 +253,43 @@ "a11y/AI is generating": "AI is generating", "a11y/AI is thinking": "AI is thinking", "a11y/Avatar of {{name}}": "Avatar of {{name}}", + "a11y/Channel with {{count}} members": "قناة بها {{count}} أعضاء", "a11y/Connected": "Connected", - "a11y/Delivered": "Delivered", + "a11y/Delivered": "تم التسليم", + "a11y/Delivered, sent by you": "تم التسليم، مُرسلة منك", + "a11y/Direct chat with {{name}}": "محادثة مباشرة مع {{name}}", + "a11y/Double tap to open": "انقر مرتين للفتح", + "a11y/Double tap to view reactions": "انقر مرتين لعرض التفاعلات", + "a11y/Editing message": "تعديل الرسالة", + "a11y/Editing message: {{text}}": "تعديل الرسالة: {{text}}", + "a11y/Last message {{date}}": "آخر رسالة {{date}}", "a11y/Loading": "Loading", "a11y/Loading failed": "Loading failed", "a11y/Message actions": "Message actions", + "a11y/Muted": "مكتوم", "a11y/New message from {{user}}": "New message from {{user}}", "a11y/Offline": "Offline", "a11y/Open message actions": "Open message actions", "a11y/Reaction {{emoji}} by {{count}} users": "Reaction {{emoji}} by {{count}} users", - "a11y/Read": "Read", + "a11y/Read": "مقروءة", + "a11y/Read, sent by you": "مقروءة، مُرسلة منك", "a11y/Reconnecting": "Reconnecting", "a11y/Reply to {{user}}": "Reply to {{user}}", "a11y/Remove edit": "Remove edit", "a11y/Remove reply": "Remove reply", + "a11y/Replying to {{user}}": "الرد على {{user}}", + "a11y/Replying to {{user}}: {{text}}": "الرد على {{user}}: {{text}}", "a11y/Scroll to bottom": "Scroll to bottom", "a11y/Scroll to bottom, {{count}} new messages": "Scroll to bottom, {{count}} new messages", "a11y/Scroll to latest": "Scroll to latest", "a11y/Scroll to latest, {{count}} unread": "Scroll to latest, {{count}} unread", "a11y/Send message": "Send message", "a11y/Sending": "Sending", - "a11y/Sent": "Sent", + "a11y/Sent": "مُرسلة", + "a11y/Sent by you": "مُرسلة منك", "a11y/Voice message recording. Hold to record.": "Voice message recording. Hold to record.", + "a11y/and {{count}} more reactions": "و{{count}} تفاعلات أخرى", + "a11y/you reacted": "تفاعلت", "a11y/{{count}} new messages": "{{count}} new messages", "a11y/Add attachment": "Add attachment", "a11y/Close attachments": "Close attachments", @@ -352,12 +367,9 @@ "size limit": "حد الحجم", "unknown error": "خطأ غير معروف", "unsupported file type": "نوع ملف غير مدعوم", - "a11y/Activate to view results": "فعّل لعرض النتائج", - "a11y/End vote": "إنهاء التصويت", - "a11y/Show all options": "إظهار جميع الخيارات", - "a11y/Vote on {{option}}": "صوّت على {{option}}", "a11y/Double tap and hold to activate contextual menu": "انقر نقرًا مزدوجًا مع الاستمرار لتفعيل قائمة السياق", "a11y/Swipe right to go through different actions": "اسحب لليمين للتنقل بين الإجراءات المختلفة", "a11y/Close": "Close", - "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss." + "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.", + "a11y/{{count}} unread messages": "{{count}} رسائل غير مقروءة" } diff --git a/package/src/i18n/en.json b/package/src/i18n/en.json index d5d3f16471..fac2d54a8f 100644 --- a/package/src/i18n/en.json +++ b/package/src/i18n/en.json @@ -253,20 +253,32 @@ "a11y/AI is generating": "AI is generating", "a11y/AI is thinking": "AI is thinking", "a11y/Avatar of {{name}}": "Avatar of {{name}}", + "a11y/Channel with {{count}} members": "Channel with {{count}} members", "a11y/Connected": "Connected", "a11y/Delivered": "Delivered", + "a11y/Delivered, sent by you": "Delivered, sent by you", + "a11y/Direct chat with {{name}}": "Direct chat with {{name}}", + "a11y/Double tap to open": "Double tap to open", + "a11y/Double tap to view reactions": "Double tap to view reactions", + "a11y/Editing message": "Editing message", + "a11y/Editing message: {{text}}": "Editing message: {{text}}", + "a11y/Last message {{date}}": "Last message {{date}}", "a11y/Loading": "Loading", "a11y/Loading failed": "Loading failed", "a11y/Message actions": "Message actions", + "a11y/Muted": "Muted", "a11y/New message from {{user}}": "New message from {{user}}", "a11y/Offline": "Offline", "a11y/Open message actions": "Open message actions", "a11y/Reaction {{emoji}} by {{count}} users": "Reaction {{emoji}} by {{count}} users", "a11y/Read": "Read", + "a11y/Read, sent by you": "Read, sent by you", "a11y/Reconnecting": "Reconnecting", "a11y/Reply to {{user}}": "Reply to {{user}}", "a11y/Remove edit": "Remove edit", "a11y/Remove reply": "Remove reply", + "a11y/Replying to {{user}}": "Replying to {{user}}", + "a11y/Replying to {{user}}: {{text}}": "Replying to {{user}}: {{text}}", "a11y/Scroll to bottom": "Scroll to bottom", "a11y/Scroll to bottom, {{count}} new messages": "Scroll to bottom, {{count}} new messages", "a11y/Scroll to latest": "Scroll to latest", @@ -274,7 +286,10 @@ "a11y/Send message": "Send message", "a11y/Sending": "Sending", "a11y/Sent": "Sent", + "a11y/Sent by you": "Sent by you", "a11y/Voice message recording. Hold to record.": "Voice message recording. Hold to record.", + "a11y/and {{count}} more reactions": "and {{count}} more reactions", + "a11y/you reacted": "you reacted", "a11y/{{count}} new messages": "{{count}} new messages", "a11y/Add attachment": "Add attachment", "a11y/Close attachments": "Close attachments", @@ -308,14 +323,10 @@ "a11y/Stop voice recording": "Stop voice recording", "a11y/Notifications": "Notifications", "a11y/Dismiss notification": "Dismiss notification", - "a11y/Activate to view results": "Activate to view results", "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.", "a11y/Close": "Close", "a11y/Double tap and hold to activate contextual menu": "Double tap and hold to activate contextual menu", - "a11y/End vote": "End vote", - "a11y/Show all options": "Show all options", "a11y/Swipe right to go through different actions": "Swipe right to go through different actions", - "a11y/Vote on {{option}}": "Vote on {{option}}", "Attachment upload blocked due to {{reason}}": "Attachment upload blocked due to {{reason}}", "Attachment upload failed due to {{reason}}": "Attachment upload failed due to {{reason}}", "Command not available": "Command not available", @@ -359,5 +370,6 @@ "{{ user }} has been unmuted": "{{ user }} has been unmuted", "size limit": "size limit", "unknown error": "unknown error", - "unsupported file type": "unsupported file type" + "unsupported file type": "unsupported file type", + "a11y/{{count}} unread messages": "{{count}} unread messages" } diff --git a/package/src/i18n/es.json b/package/src/i18n/es.json index 75fdc566e5..3755ab3a59 100644 --- a/package/src/i18n/es.json +++ b/package/src/i18n/es.json @@ -253,20 +253,32 @@ "a11y/AI is generating": "La IA está generando", "a11y/AI is thinking": "La IA está pensando", "a11y/Avatar of {{name}}": "Avatar de {{name}}", + "a11y/Channel with {{count}} members": "Canal con {{count}} miembros", "a11y/Connected": "Conectado", "a11y/Delivered": "Entregado", + "a11y/Delivered, sent by you": "Entregado, enviado por ti", + "a11y/Direct chat with {{name}}": "Chat directo con {{name}}", + "a11y/Double tap to open": "Toca dos veces para abrir", + "a11y/Double tap to view reactions": "Toca dos veces para ver las reacciones", + "a11y/Editing message": "Editando mensaje", + "a11y/Editing message: {{text}}": "Editando mensaje: {{text}}", + "a11y/Last message {{date}}": "Último mensaje {{date}}", "a11y/Loading": "Cargando", "a11y/Loading failed": "Error al cargar", "a11y/Message actions": "Acciones del mensaje", + "a11y/Muted": "Silenciado", "a11y/New message from {{user}}": "Nuevo mensaje de {{user}}", "a11y/Offline": "Sin conexión", "a11y/Open message actions": "Abrir acciones del mensaje", "a11y/Reaction {{emoji}} by {{count}} users": "Reacción {{emoji}} de {{count}} usuarios", "a11y/Read": "Leído", + "a11y/Read, sent by you": "Leído, enviado por ti", "a11y/Reconnecting": "Reconectando", "a11y/Reply to {{user}}": "Responder a {{user}}", "a11y/Remove edit": "Eliminar edición", "a11y/Remove reply": "Eliminar respuesta", + "a11y/Replying to {{user}}": "Respondiendo a {{user}}", + "a11y/Replying to {{user}}: {{text}}": "Respondiendo a {{user}}: {{text}}", "a11y/Scroll to bottom": "Ir al final", "a11y/Scroll to bottom, {{count}} new messages": "Ir al final, {{count}} mensajes nuevos", "a11y/Scroll to latest": "Ir al último mensaje", @@ -274,7 +286,10 @@ "a11y/Send message": "Enviar mensaje", "a11y/Sending": "Enviando", "a11y/Sent": "Enviado", + "a11y/Sent by you": "Enviado por ti", "a11y/Voice message recording. Hold to record.": "Grabación de mensaje de voz. Mantén pulsado para grabar.", + "a11y/and {{count}} more reactions": "y {{count}} reacciones más", + "a11y/you reacted": "tú reaccionaste", "a11y/{{count}} new messages": "{{count}} mensajes nuevos", "a11y/Add attachment": "Add attachment", "a11y/Close attachments": "Close attachments", @@ -352,12 +367,9 @@ "size limit": "límite de tamaño", "unknown error": "error desconocido", "unsupported file type": "tipo de archivo no compatible", - "a11y/Activate to view results": "Activa para ver los resultados", - "a11y/End vote": "Finalizar votación", - "a11y/Show all options": "Mostrar todas las opciones", - "a11y/Vote on {{option}}": "Votar por {{option}}", "a11y/Double tap and hold to activate contextual menu": "Toca dos veces y mantén pulsado para activar el menú contextual", "a11y/Swipe right to go through different actions": "Desliza a la derecha para recorrer las diferentes acciones", "a11y/Close": "Close", - "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss." + "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.", + "a11y/{{count}} unread messages": "{{count}} mensajes sin leer" } diff --git a/package/src/i18n/fr.json b/package/src/i18n/fr.json index bc498a9f71..0c264d6978 100644 --- a/package/src/i18n/fr.json +++ b/package/src/i18n/fr.json @@ -253,20 +253,32 @@ "a11y/AI is generating": "L'IA génère une réponse", "a11y/AI is thinking": "L'IA réfléchit", "a11y/Avatar of {{name}}": "Avatar de {{name}}", + "a11y/Channel with {{count}} members": "Canal avec {{count}} membres", "a11y/Connected": "Connecté", "a11y/Delivered": "Distribué", + "a11y/Delivered, sent by you": "Distribué, envoyé par vous", + "a11y/Direct chat with {{name}}": "Discussion directe avec {{name}}", + "a11y/Double tap to open": "Appuyez deux fois pour ouvrir", + "a11y/Double tap to view reactions": "Appuyez deux fois pour voir les réactions", + "a11y/Editing message": "Modification du message", + "a11y/Editing message: {{text}}": "Modification du message : {{text}}", + "a11y/Last message {{date}}": "Dernier message {{date}}", "a11y/Loading": "Chargement", "a11y/Loading failed": "Échec du chargement", "a11y/Message actions": "Actions du message", + "a11y/Muted": "Mis en sourdine", "a11y/New message from {{user}}": "Nouveau message de {{user}}", "a11y/Offline": "Hors ligne", "a11y/Open message actions": "Ouvrir les actions du message", "a11y/Reaction {{emoji}} by {{count}} users": "Réaction {{emoji}} par {{count}} utilisateurs", "a11y/Read": "Lu", + "a11y/Read, sent by you": "Lu, envoyé par vous", "a11y/Reconnecting": "Reconnexion", "a11y/Reply to {{user}}": "Répondre à {{user}}", "a11y/Remove edit": "Supprimer la modification", "a11y/Remove reply": "Supprimer la réponse", + "a11y/Replying to {{user}}": "Réponse à {{user}}", + "a11y/Replying to {{user}}: {{text}}": "Réponse à {{user}} : {{text}}", "a11y/Scroll to bottom": "Aller en bas", "a11y/Scroll to bottom, {{count}} new messages": "Aller en bas, {{count}} nouveaux messages", "a11y/Scroll to latest": "Aller au dernier message", @@ -274,7 +286,10 @@ "a11y/Send message": "Envoyer le message", "a11y/Sending": "Envoi", "a11y/Sent": "Envoyé", + "a11y/Sent by you": "Envoyé par vous", "a11y/Voice message recording. Hold to record.": "Enregistrement d'un message vocal. Maintenez appuyé pour enregistrer.", + "a11y/and {{count}} more reactions": "et {{count}} réactions de plus", + "a11y/you reacted": "vous avez réagi", "a11y/{{count}} new messages": "{{count}} nouveaux messages", "a11y/Add attachment": "Add attachment", "a11y/Close attachments": "Close attachments", @@ -352,12 +367,9 @@ "size limit": "limite de taille", "unknown error": "erreur inconnue", "unsupported file type": "type de fichier non pris en charge", - "a11y/Activate to view results": "Activer pour voir les résultats", - "a11y/End vote": "Terminer le vote", - "a11y/Show all options": "Afficher toutes les options", - "a11y/Vote on {{option}}": "Voter pour {{option}}", "a11y/Double tap and hold to activate contextual menu": "Appuyez deux fois et maintenez pour activer le menu contextuel", "a11y/Swipe right to go through different actions": "Glissez vers la droite pour parcourir les différentes actions", "a11y/Close": "Close", - "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss." + "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.", + "a11y/{{count}} unread messages": "{{count}} messages non lus" } diff --git a/package/src/i18n/he.json b/package/src/i18n/he.json index 42c9e51ee0..b59b165300 100644 --- a/package/src/i18n/he.json +++ b/package/src/i18n/he.json @@ -253,20 +253,32 @@ "a11y/AI is generating": "הבינה המלאכותית יוצרת תשובה", "a11y/AI is thinking": "הבינה המלאכותית חושבת", "a11y/Avatar of {{name}}": "תמונת פרופיל של {{name}}", + "a11y/Channel with {{count}} members": "ערוץ עם {{count}} חברים", "a11y/Connected": "מחובר", "a11y/Delivered": "נמסר", + "a11y/Delivered, sent by you": "נמסר, נשלח על ידך", + "a11y/Direct chat with {{name}}": "צ׳אט ישיר עם {{name}}", + "a11y/Double tap to open": "הקש פעמיים כדי לפתוח", + "a11y/Double tap to view reactions": "הקש פעמיים כדי לראות תגובות", + "a11y/Editing message": "עריכת הודעה", + "a11y/Editing message: {{text}}": "עריכת הודעה: {{text}}", + "a11y/Last message {{date}}": "הודעה אחרונה {{date}}", "a11y/Loading": "טוען", "a11y/Loading failed": "הטעינה נכשלה", "a11y/Message actions": "פעולות הודעה", + "a11y/Muted": "מושתק", "a11y/New message from {{user}}": "הודעה חדשה מ-{{user}}", "a11y/Offline": "לא מקוון", "a11y/Open message actions": "פתח פעולות הודעה", "a11y/Reaction {{emoji}} by {{count}} users": "תגובה {{emoji}} מאת {{count}} משתמשים", "a11y/Read": "נקרא", + "a11y/Read, sent by you": "נקרא, נשלח על ידך", "a11y/Reconnecting": "מתחבר מחדש", "a11y/Reply to {{user}}": "השב ל-{{user}}", "a11y/Remove edit": "הסר עריכה", "a11y/Remove reply": "הסר תגובה", + "a11y/Replying to {{user}}": "מגיב/ה ל-{{user}}", + "a11y/Replying to {{user}}: {{text}}": "מגיב/ה ל-{{user}}: {{text}}", "a11y/Scroll to bottom": "גלול לתחתית", "a11y/Scroll to bottom, {{count}} new messages": "גלול לתחתית, {{count}} הודעות חדשות", "a11y/Scroll to latest": "גלול להודעה האחרונה", @@ -274,7 +286,10 @@ "a11y/Send message": "שלח הודעה", "a11y/Sending": "שולח", "a11y/Sent": "נשלח", + "a11y/Sent by you": "נשלח על ידך", "a11y/Voice message recording. Hold to record.": "הקלטת הודעה קולית. החזק כדי להקליט.", + "a11y/and {{count}} more reactions": "ועוד {{count}} תגובות", + "a11y/you reacted": "הגבת", "a11y/{{count}} new messages": "{{count}} הודעות חדשות", "a11y/Add attachment": "Add attachment", "a11y/Close attachments": "Close attachments", @@ -352,12 +367,9 @@ "size limit": "מגבלת גודל", "unknown error": "שגיאה לא ידועה", "unsupported file type": "סוג קובץ לא נתמך", - "a11y/Activate to view results": "הפעל כדי לראות את התוצאות", - "a11y/End vote": "סיים הצבעה", - "a11y/Show all options": "הצג את כל האפשרויות", - "a11y/Vote on {{option}}": "הצבע עבור {{option}}", "a11y/Double tap and hold to activate contextual menu": "הקש פעמיים והחזק כדי להפעיל את התפריט ההקשרי", "a11y/Swipe right to go through different actions": "החלק ימינה כדי לעבור בין הפעולות השונות", "a11y/Close": "Close", - "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss." + "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.", + "a11y/{{count}} unread messages": "{{count}} הודעות שלא נקראו" } diff --git a/package/src/i18n/hi.json b/package/src/i18n/hi.json index 84586cafd8..aeed1621c3 100644 --- a/package/src/i18n/hi.json +++ b/package/src/i18n/hi.json @@ -253,20 +253,32 @@ "a11y/AI is generating": "AI जवाब तैयार कर रहा है", "a11y/AI is thinking": "AI सोच रहा है", "a11y/Avatar of {{name}}": "{{name}} का अवतार", + "a11y/Channel with {{count}} members": "{{count}} सदस्यों वाला चैनल", "a11y/Connected": "कनेक्टेड", - "a11y/Delivered": "डिलीवर हुआ", + "a11y/Delivered": "डिलीवर हो गया", + "a11y/Delivered, sent by you": "डिलीवर हो गया, आपके द्वारा भेजा गया", + "a11y/Direct chat with {{name}}": "{{name}} के साथ सीधी चैट", + "a11y/Double tap to open": "खोलने के लिए दो बार टैप करें", + "a11y/Double tap to view reactions": "प्रतिक्रियाएँ देखने के लिए दो बार टैप करें", + "a11y/Editing message": "संदेश संपादित कर रहे हैं", + "a11y/Editing message: {{text}}": "संदेश संपादित कर रहे हैं: {{text}}", + "a11y/Last message {{date}}": "अंतिम संदेश {{date}}", "a11y/Loading": "लोड हो रहा है", "a11y/Loading failed": "लोड नहीं हो सका", "a11y/Message actions": "संदेश की कार्रवाइयां", + "a11y/Muted": "म्यूट किया गया", "a11y/New message from {{user}}": "{{user}} से नया संदेश", "a11y/Offline": "ऑफलाइन", "a11y/Open message actions": "संदेश की कार्रवाइयां खोलें", "a11y/Reaction {{emoji}} by {{count}} users": "{{count}} उपयोगकर्ताओं की {{emoji}} प्रतिक्रिया", "a11y/Read": "पढ़ा गया", + "a11y/Read, sent by you": "पढ़ा गया, आपके द्वारा भेजा गया", "a11y/Reconnecting": "फिर से कनेक्ट हो रहा है", "a11y/Reply to {{user}}": "{{user}} को जवाब दें", "a11y/Remove edit": "संपादन हटाएं", "a11y/Remove reply": "जवाब हटाएं", + "a11y/Replying to {{user}}": "{{user}} को उत्तर दे रहे हैं", + "a11y/Replying to {{user}}: {{text}}": "{{user}} को उत्तर दे रहे हैं: {{text}}", "a11y/Scroll to bottom": "नीचे स्क्रॉल करें", "a11y/Scroll to bottom, {{count}} new messages": "नीचे स्क्रॉल करें, {{count}} नए संदेश", "a11y/Scroll to latest": "नवीनतम संदेश पर जाएं", @@ -274,7 +286,10 @@ "a11y/Send message": "संदेश भेजें", "a11y/Sending": "भेजा जा रहा है", "a11y/Sent": "भेजा गया", + "a11y/Sent by you": "आपके द्वारा भेजा गया", "a11y/Voice message recording. Hold to record.": "वॉइस संदेश रिकॉर्डिंग। रिकॉर्ड करने के लिए दबाकर रखें।", + "a11y/and {{count}} more reactions": "और {{count}} प्रतिक्रियाएँ", + "a11y/you reacted": "आपने प्रतिक्रिया दी", "a11y/{{count}} new messages": "{{count}} नए संदेश", "a11y/Add attachment": "Add attachment", "a11y/Close attachments": "Close attachments", @@ -352,12 +367,9 @@ "size limit": "आकार सीमा", "unknown error": "अज्ञात त्रुटि", "unsupported file type": "असमर्थित फ़ाइल प्रकार", - "a11y/Activate to view results": "परिणाम देखने के लिए सक्रिय करें", - "a11y/End vote": "मतदान समाप्त करें", - "a11y/Show all options": "सभी विकल्प दिखाएं", - "a11y/Vote on {{option}}": "{{option}} पर वोट करें", "a11y/Double tap and hold to activate contextual menu": "संदर्भ मेनू सक्रिय करने के लिए दो बार टैप करें और होल्ड करें", "a11y/Swipe right to go through different actions": "विभिन्न क्रियाओं के बीच जाने के लिए दाएं स्वाइप करें", "a11y/Close": "Close", - "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss." + "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.", + "a11y/{{count}} unread messages": "{{count}} अपठित संदेश" } diff --git a/package/src/i18n/it.json b/package/src/i18n/it.json index 4452fb0009..cafe8dcdf8 100644 --- a/package/src/i18n/it.json +++ b/package/src/i18n/it.json @@ -253,20 +253,32 @@ "a11y/AI is generating": "L'IA sta generando", "a11y/AI is thinking": "L'IA sta pensando", "a11y/Avatar of {{name}}": "Avatar di {{name}}", + "a11y/Channel with {{count}} members": "Canale con {{count}} membri", "a11y/Connected": "Connesso", "a11y/Delivered": "Consegnato", + "a11y/Delivered, sent by you": "Consegnato, inviato da te", + "a11y/Direct chat with {{name}}": "Chat diretta con {{name}}", + "a11y/Double tap to open": "Tocca due volte per aprire", + "a11y/Double tap to view reactions": "Tocca due volte per vedere le reazioni", + "a11y/Editing message": "Modifica del messaggio", + "a11y/Editing message: {{text}}": "Modifica del messaggio: {{text}}", + "a11y/Last message {{date}}": "Ultimo messaggio {{date}}", "a11y/Loading": "Caricamento", "a11y/Loading failed": "Caricamento non riuscito", "a11y/Message actions": "Azioni del messaggio", + "a11y/Muted": "Silenziato", "a11y/New message from {{user}}": "Nuovo messaggio da {{user}}", "a11y/Offline": "Offline", "a11y/Open message actions": "Apri azioni del messaggio", "a11y/Reaction {{emoji}} by {{count}} users": "Reazione {{emoji}} di {{count}} utenti", "a11y/Read": "Letto", + "a11y/Read, sent by you": "Letto, inviato da te", "a11y/Reconnecting": "Riconnessione", "a11y/Reply to {{user}}": "Rispondi a {{user}}", "a11y/Remove edit": "Rimuovi modifica", "a11y/Remove reply": "Rimuovi risposta", + "a11y/Replying to {{user}}": "Rispondendo a {{user}}", + "a11y/Replying to {{user}}: {{text}}": "Rispondendo a {{user}}: {{text}}", "a11y/Scroll to bottom": "Vai in fondo", "a11y/Scroll to bottom, {{count}} new messages": "Vai in fondo, {{count}} nuovi messaggi", "a11y/Scroll to latest": "Vai al messaggio più recente", @@ -274,7 +286,10 @@ "a11y/Send message": "Invia messaggio", "a11y/Sending": "Invio in corso", "a11y/Sent": "Inviato", + "a11y/Sent by you": "Inviato da te", "a11y/Voice message recording. Hold to record.": "Registrazione del messaggio vocale. Tieni premuto per registrare.", + "a11y/and {{count}} more reactions": "e altre {{count}} reazioni", + "a11y/you reacted": "hai reagito", "a11y/{{count}} new messages": "{{count}} nuovi messaggi", "a11y/Add attachment": "Add attachment", "a11y/Close attachments": "Close attachments", @@ -352,12 +367,9 @@ "size limit": "limite di dimensione", "unknown error": "errore sconosciuto", "unsupported file type": "tipo di file non supportato", - "a11y/Activate to view results": "Attiva per vedere i risultati", - "a11y/End vote": "Termina sondaggio", - "a11y/Show all options": "Mostra tutte le opzioni", - "a11y/Vote on {{option}}": "Vota per {{option}}", "a11y/Double tap and hold to activate contextual menu": "Tocca due volte e tieni premuto per attivare il menu contestuale", "a11y/Swipe right to go through different actions": "Scorri a destra per passare in rassegna le diverse azioni", "a11y/Close": "Close", - "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss." + "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.", + "a11y/{{count}} unread messages": "{{count}} messaggi non letti" } diff --git a/package/src/i18n/ja.json b/package/src/i18n/ja.json index 73f05ecc4a..d9ba98adf1 100644 --- a/package/src/i18n/ja.json +++ b/package/src/i18n/ja.json @@ -253,20 +253,32 @@ "a11y/AI is generating": "AIが生成しています", "a11y/AI is thinking": "AIが考えています", "a11y/Avatar of {{name}}": "{{name}}のアバター", + "a11y/Channel with {{count}} members": "メンバー{{count}}人のチャンネル", "a11y/Connected": "接続済み", "a11y/Delivered": "配信済み", + "a11y/Delivered, sent by you": "配信済み、あなたから送信", + "a11y/Direct chat with {{name}}": "{{name}}とのダイレクトチャット", + "a11y/Double tap to open": "ダブルタップで開く", + "a11y/Double tap to view reactions": "ダブルタップでリアクションを表示", + "a11y/Editing message": "メッセージを編集中", + "a11y/Editing message: {{text}}": "メッセージを編集中: {{text}}", + "a11y/Last message {{date}}": "最後のメッセージ {{date}}", "a11y/Loading": "読み込み中", "a11y/Loading failed": "読み込みに失敗しました", "a11y/Message actions": "メッセージの操作", + "a11y/Muted": "ミュート中", "a11y/New message from {{user}}": "{{user}}からの新しいメッセージ", "a11y/Offline": "オフライン", "a11y/Open message actions": "メッセージの操作を開く", "a11y/Reaction {{emoji}} by {{count}} users": "{{count}}人のユーザーによるリアクション{{emoji}}", "a11y/Read": "既読", + "a11y/Read, sent by you": "既読、あなたから送信", "a11y/Reconnecting": "再接続中", "a11y/Reply to {{user}}": "{{user}}に返信", "a11y/Remove edit": "編集を削除", "a11y/Remove reply": "返信を削除", + "a11y/Replying to {{user}}": "{{user}}に返信中", + "a11y/Replying to {{user}}: {{text}}": "{{user}}に返信中: {{text}}", "a11y/Scroll to bottom": "一番下へ移動", "a11y/Scroll to bottom, {{count}} new messages": "一番下へ移動、新しいメッセージ{{count}}件", "a11y/Scroll to latest": "最新のメッセージへ移動", @@ -274,7 +286,10 @@ "a11y/Send message": "メッセージを送信", "a11y/Sending": "送信中", "a11y/Sent": "送信済み", + "a11y/Sent by you": "あなたから送信済み", "a11y/Voice message recording. Hold to record.": "音声メッセージの録音。長押しして録音します。", + "a11y/and {{count}} more reactions": "および{{count}}件のリアクション", + "a11y/you reacted": "あなたがリアクション", "a11y/{{count}} new messages": "新しいメッセージ{{count}}件", "a11y/Add attachment": "Add attachment", "a11y/Close attachments": "Close attachments", @@ -352,12 +367,9 @@ "size limit": "サイズ制限", "unknown error": "不明なエラー", "unsupported file type": "サポートされていないファイル形式", - "a11y/Activate to view results": "結果を表示するには有効化", - "a11y/End vote": "投票を終了", - "a11y/Show all options": "すべてのオプションを表示", - "a11y/Vote on {{option}}": "{{option}}に投票", "a11y/Double tap and hold to activate contextual menu": "コンテキストメニューを表示するにはダブルタップして長押し", "a11y/Swipe right to go through different actions": "右にスワイプして異なるアクションを切り替えます", "a11y/Close": "Close", - "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss." + "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.", + "a11y/{{count}} unread messages": "未読メッセージ{{count}}件" } diff --git a/package/src/i18n/ko.json b/package/src/i18n/ko.json index bec7e20f17..a9dfc16186 100644 --- a/package/src/i18n/ko.json +++ b/package/src/i18n/ko.json @@ -253,20 +253,32 @@ "a11y/AI is generating": "AI가 생성 중입니다", "a11y/AI is thinking": "AI가 생각 중입니다", "a11y/Avatar of {{name}}": "{{name}}의 아바타", + "a11y/Channel with {{count}} members": "멤버 {{count}}명의 채널", "a11y/Connected": "연결됨", "a11y/Delivered": "전달됨", + "a11y/Delivered, sent by you": "전달됨, 내가 보냄", + "a11y/Direct chat with {{name}}": "{{name}}님과의 다이렉트 채팅", + "a11y/Double tap to open": "두 번 탭하여 열기", + "a11y/Double tap to view reactions": "두 번 탭하여 반응 보기", + "a11y/Editing message": "메시지 편집 중", + "a11y/Editing message: {{text}}": "메시지 편집 중: {{text}}", + "a11y/Last message {{date}}": "마지막 메시지 {{date}}", "a11y/Loading": "로드 중", "a11y/Loading failed": "로드 실패", "a11y/Message actions": "메시지 작업", + "a11y/Muted": "음소거됨", "a11y/New message from {{user}}": "{{user}}님의 새 메시지", "a11y/Offline": "오프라인", "a11y/Open message actions": "메시지 작업 열기", "a11y/Reaction {{emoji}} by {{count}} users": "{{count}}명의 사용자가 남긴 {{emoji}} 반응", "a11y/Read": "읽음", + "a11y/Read, sent by you": "읽음, 내가 보냄", "a11y/Reconnecting": "다시 연결 중", "a11y/Reply to {{user}}": "{{user}}님에게 답장", "a11y/Remove edit": "편집 제거", "a11y/Remove reply": "답장 제거", + "a11y/Replying to {{user}}": "{{user}}님에게 답장 중", + "a11y/Replying to {{user}}: {{text}}": "{{user}}님에게 답장 중: {{text}}", "a11y/Scroll to bottom": "맨 아래로 이동", "a11y/Scroll to bottom, {{count}} new messages": "맨 아래로 이동, 새 메시지 {{count}}개", "a11y/Scroll to latest": "최신 메시지로 이동", @@ -274,7 +286,10 @@ "a11y/Send message": "메시지 보내기", "a11y/Sending": "보내는 중", "a11y/Sent": "보냄", + "a11y/Sent by you": "내가 보냄", "a11y/Voice message recording. Hold to record.": "음성 메시지 녹음. 길게 눌러 녹음하세요.", + "a11y/and {{count}} more reactions": "및 {{count}}개의 추가 반응", + "a11y/you reacted": "내가 반응함", "a11y/{{count}} new messages": "새 메시지 {{count}}개", "a11y/Add attachment": "Add attachment", "a11y/Close attachments": "Close attachments", @@ -352,12 +367,9 @@ "size limit": "크기 제한", "unknown error": "알 수 없는 오류", "unsupported file type": "지원되지 않는 파일 형식", - "a11y/Activate to view results": "결과를 보려면 활성화", - "a11y/End vote": "투표 종료", - "a11y/Show all options": "모든 옵션 표시", - "a11y/Vote on {{option}}": "{{option}}에 투표", "a11y/Double tap and hold to activate contextual menu": "컨텍스트 메뉴를 활성화하려면 두 번 탭하고 길게 누르세요", "a11y/Swipe right to go through different actions": "다른 작업을 탐색하려면 오른쪽으로 스와이프하세요", "a11y/Close": "Close", - "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss." + "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.", + "a11y/{{count}} unread messages": "읽지 않은 메시지 {{count}}개" } diff --git a/package/src/i18n/nl.json b/package/src/i18n/nl.json index 1d0a400967..e748128250 100644 --- a/package/src/i18n/nl.json +++ b/package/src/i18n/nl.json @@ -253,20 +253,32 @@ "a11y/AI is generating": "AI genereert", "a11y/AI is thinking": "AI denkt na", "a11y/Avatar of {{name}}": "Avatar van {{name}}", + "a11y/Channel with {{count}} members": "Kanaal met {{count}} leden", "a11y/Connected": "Verbonden", - "a11y/Delivered": "Afgeleverd", + "a11y/Delivered": "Bezorgd", + "a11y/Delivered, sent by you": "Bezorgd, door jou verzonden", + "a11y/Direct chat with {{name}}": "Direct chat met {{name}}", + "a11y/Double tap to open": "Dubbeltik om te openen", + "a11y/Double tap to view reactions": "Dubbeltik om reacties te bekijken", + "a11y/Editing message": "Bericht bewerken", + "a11y/Editing message: {{text}}": "Bericht bewerken: {{text}}", + "a11y/Last message {{date}}": "Laatste bericht {{date}}", "a11y/Loading": "Laden", "a11y/Loading failed": "Laden mislukt", "a11y/Message actions": "Berichtacties", + "a11y/Muted": "Gedempt", "a11y/New message from {{user}}": "Nieuw bericht van {{user}}", "a11y/Offline": "Offline", "a11y/Open message actions": "Berichtacties openen", "a11y/Reaction {{emoji}} by {{count}} users": "Reactie {{emoji}} door {{count}} gebruikers", "a11y/Read": "Gelezen", + "a11y/Read, sent by you": "Gelezen, door jou verzonden", "a11y/Reconnecting": "Opnieuw verbinden", "a11y/Reply to {{user}}": "Antwoorden op {{user}}", "a11y/Remove edit": "Bewerking verwijderen", "a11y/Remove reply": "Antwoord verwijderen", + "a11y/Replying to {{user}}": "Antwoorden op {{user}}", + "a11y/Replying to {{user}}: {{text}}": "Antwoorden op {{user}}: {{text}}", "a11y/Scroll to bottom": "Ga naar beneden", "a11y/Scroll to bottom, {{count}} new messages": "Ga naar beneden, {{count}} nieuwe berichten", "a11y/Scroll to latest": "Ga naar het nieuwste bericht", @@ -274,7 +286,10 @@ "a11y/Send message": "Bericht verzenden", "a11y/Sending": "Verzenden", "a11y/Sent": "Verzonden", + "a11y/Sent by you": "Door jou verzonden", "a11y/Voice message recording. Hold to record.": "Spraakbericht opnemen. Houd ingedrukt om op te nemen.", + "a11y/and {{count}} more reactions": "en {{count}} meer reacties", + "a11y/you reacted": "jij hebt gereageerd", "a11y/{{count}} new messages": "{{count}} nieuwe berichten", "a11y/Add attachment": "Add attachment", "a11y/Close attachments": "Close attachments", @@ -352,12 +367,9 @@ "size limit": "groottelimiet", "unknown error": "onbekende fout", "unsupported file type": "niet-ondersteund bestandstype", - "a11y/Activate to view results": "Activeer om resultaten te bekijken", - "a11y/End vote": "Stemming beëindigen", - "a11y/Show all options": "Alle opties weergeven", - "a11y/Vote on {{option}}": "Stem op {{option}}", "a11y/Double tap and hold to activate contextual menu": "Dubbeltik en houd vast om het contextmenu te openen", "a11y/Swipe right to go through different actions": "Veeg naar rechts om door verschillende acties te bladeren", "a11y/Close": "Close", - "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss." + "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.", + "a11y/{{count}} unread messages": "{{count}} ongelezen berichten" } diff --git a/package/src/i18n/pt-br.json b/package/src/i18n/pt-br.json index 54da88ca23..149dd79384 100644 --- a/package/src/i18n/pt-br.json +++ b/package/src/i18n/pt-br.json @@ -253,28 +253,43 @@ "a11y/AI is generating": "A IA está gerando", "a11y/AI is thinking": "A IA está pensando", "a11y/Avatar of {{name}}": "Avatar de {{name}}", + "a11y/Channel with {{count}} members": "Canal com {{count}} membros", "a11y/Connected": "Conectado", "a11y/Delivered": "Entregue", + "a11y/Delivered, sent by you": "Entregue, enviada por você", + "a11y/Direct chat with {{name}}": "Chat direto com {{name}}", + "a11y/Double tap to open": "Toque duas vezes para abrir", + "a11y/Double tap to view reactions": "Toque duas vezes para ver as reações", + "a11y/Editing message": "Editando mensagem", + "a11y/Editing message: {{text}}": "Editando mensagem: {{text}}", + "a11y/Last message {{date}}": "Última mensagem {{date}}", "a11y/Loading": "Carregando", "a11y/Loading failed": "Falha ao carregar", "a11y/Message actions": "Ações da mensagem", + "a11y/Muted": "Silenciado", "a11y/New message from {{user}}": "Nova mensagem de {{user}}", "a11y/Offline": "Offline", "a11y/Open message actions": "Abrir ações da mensagem", "a11y/Reaction {{emoji}} by {{count}} users": "Reação {{emoji}} de {{count}} usuários", - "a11y/Read": "Lido", + "a11y/Read": "Lida", + "a11y/Read, sent by you": "Lida, enviada por você", "a11y/Reconnecting": "Reconectando", "a11y/Reply to {{user}}": "Responder a {{user}}", "a11y/Remove edit": "Remover edição", "a11y/Remove reply": "Remover resposta", + "a11y/Replying to {{user}}": "Respondendo a {{user}}", + "a11y/Replying to {{user}}: {{text}}": "Respondendo a {{user}}: {{text}}", "a11y/Scroll to bottom": "Ir para o final", "a11y/Scroll to bottom, {{count}} new messages": "Ir para o final, {{count}} novas mensagens", "a11y/Scroll to latest": "Ir para a mensagem mais recente", "a11y/Scroll to latest, {{count}} unread": "Ir para a mensagem mais recente, {{count}} não lidas", "a11y/Send message": "Enviar mensagem", "a11y/Sending": "Enviando", - "a11y/Sent": "Enviado", + "a11y/Sent": "Enviada", + "a11y/Sent by you": "Enviada por você", "a11y/Voice message recording. Hold to record.": "Gravação de mensagem de voz. Mantenha pressionado para gravar.", + "a11y/and {{count}} more reactions": "e mais {{count}} reações", + "a11y/you reacted": "você reagiu", "a11y/{{count}} new messages": "{{count}} novas mensagens", "a11y/Add attachment": "Add attachment", "a11y/Close attachments": "Close attachments", @@ -352,12 +367,9 @@ "size limit": "limite de tamanho", "unknown error": "erro desconhecido", "unsupported file type": "tipo de arquivo não compatível", - "a11y/Activate to view results": "Ative para ver os resultados", - "a11y/End vote": "Encerrar votação", - "a11y/Show all options": "Mostrar todas as opções", - "a11y/Vote on {{option}}": "Votar em {{option}}", "a11y/Double tap and hold to activate contextual menu": "Toque duas vezes e segure para ativar o menu contextual", "a11y/Swipe right to go through different actions": "Deslize para a direita para percorrer as diferentes ações", "a11y/Close": "Close", - "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss." + "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.", + "a11y/{{count}} unread messages": "{{count}} mensagens não lidas" } diff --git a/package/src/i18n/ru.json b/package/src/i18n/ru.json index 32b2f456ed..0040618446 100644 --- a/package/src/i18n/ru.json +++ b/package/src/i18n/ru.json @@ -253,20 +253,32 @@ "a11y/AI is generating": "ИИ генерирует ответ", "a11y/AI is thinking": "ИИ думает", "a11y/Avatar of {{name}}": "Аватар {{name}}", + "a11y/Channel with {{count}} members": "Канал с {{count}} участниками", "a11y/Connected": "Подключено", "a11y/Delivered": "Доставлено", + "a11y/Delivered, sent by you": "Доставлено, отправлено вами", + "a11y/Direct chat with {{name}}": "Прямой чат с {{name}}", + "a11y/Double tap to open": "Дважды коснитесь, чтобы открыть", + "a11y/Double tap to view reactions": "Дважды коснитесь, чтобы посмотреть реакции", + "a11y/Editing message": "Редактирование сообщения", + "a11y/Editing message: {{text}}": "Редактирование сообщения: {{text}}", + "a11y/Last message {{date}}": "Последнее сообщение {{date}}", "a11y/Loading": "Загрузка", "a11y/Loading failed": "Не удалось загрузить", "a11y/Message actions": "Действия с сообщением", + "a11y/Muted": "Без звука", "a11y/New message from {{user}}": "Новое сообщение от {{user}}", "a11y/Offline": "Не в сети", "a11y/Open message actions": "Открыть действия с сообщением", "a11y/Reaction {{emoji}} by {{count}} users": "Реакция {{emoji}} от {{count}} пользователей", "a11y/Read": "Прочитано", + "a11y/Read, sent by you": "Прочитано, отправлено вами", "a11y/Reconnecting": "Повторное подключение", "a11y/Reply to {{user}}": "Ответить {{user}}", "a11y/Remove edit": "Удалить редактирование", "a11y/Remove reply": "Удалить ответ", + "a11y/Replying to {{user}}": "Ответ {{user}}", + "a11y/Replying to {{user}}: {{text}}": "Ответ {{user}}: {{text}}", "a11y/Scroll to bottom": "Прокрутить вниз", "a11y/Scroll to bottom, {{count}} new messages": "Прокрутить вниз, {{count}} новых сообщений", "a11y/Scroll to latest": "Перейти к последнему сообщению", @@ -274,7 +286,10 @@ "a11y/Send message": "Отправить сообщение", "a11y/Sending": "Отправка", "a11y/Sent": "Отправлено", + "a11y/Sent by you": "Отправлено вами", "a11y/Voice message recording. Hold to record.": "Запись голосового сообщения. Удерживайте, чтобы записать.", + "a11y/and {{count}} more reactions": "и ещё {{count}} реакций", + "a11y/you reacted": "вы отреагировали", "a11y/{{count}} new messages": "{{count}} новых сообщений", "a11y/Add attachment": "Add attachment", "a11y/Close attachments": "Close attachments", @@ -352,12 +367,9 @@ "size limit": "лимит размера", "unknown error": "неизвестная ошибка", "unsupported file type": "неподдерживаемый тип файла", - "a11y/Activate to view results": "Активируйте, чтобы увидеть результаты", - "a11y/End vote": "Завершить голосование", - "a11y/Show all options": "Показать все варианты", - "a11y/Vote on {{option}}": "Голосовать за {{option}}", "a11y/Double tap and hold to activate contextual menu": "Дважды коснитесь и удерживайте, чтобы открыть контекстное меню", "a11y/Swipe right to go through different actions": "Смахните вправо, чтобы переключаться между действиями", "a11y/Close": "Close", - "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss." + "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.", + "a11y/{{count}} unread messages": "{{count}} непрочитанных сообщений" } diff --git a/package/src/i18n/tr.json b/package/src/i18n/tr.json index b3a80c6a3f..9008b4dd16 100644 --- a/package/src/i18n/tr.json +++ b/package/src/i18n/tr.json @@ -253,20 +253,32 @@ "a11y/AI is generating": "Yapay zeka oluşturuyor", "a11y/AI is thinking": "Yapay zeka düşünüyor", "a11y/Avatar of {{name}}": "{{name}} avatarı", + "a11y/Channel with {{count}} members": "{{count}} üyeli kanal", "a11y/Connected": "Bağlandı", - "a11y/Delivered": "Teslim edildi", + "a11y/Delivered": "İletildi", + "a11y/Delivered, sent by you": "İletildi, sizin tarafınızdan gönderildi", + "a11y/Direct chat with {{name}}": "{{name}} ile doğrudan sohbet", + "a11y/Double tap to open": "Açmak için iki kez dokun", + "a11y/Double tap to view reactions": "Tepkileri görmek için iki kez dokun", + "a11y/Editing message": "Mesaj düzenleniyor", + "a11y/Editing message: {{text}}": "Mesaj düzenleniyor: {{text}}", + "a11y/Last message {{date}}": "Son mesaj {{date}}", "a11y/Loading": "Yükleniyor", "a11y/Loading failed": "Yükleme başarısız", "a11y/Message actions": "Mesaj eylemleri", + "a11y/Muted": "Sessize alındı", "a11y/New message from {{user}}": "{{user}} kullanıcısından yeni mesaj", "a11y/Offline": "Çevrimdışı", "a11y/Open message actions": "Mesaj eylemlerini aç", "a11y/Reaction {{emoji}} by {{count}} users": "{{count}} kullanıcıdan {{emoji}} tepkisi", "a11y/Read": "Okundu", + "a11y/Read, sent by you": "Okundu, sizin tarafınızdan gönderildi", "a11y/Reconnecting": "Yeniden bağlanıyor", "a11y/Reply to {{user}}": "{{user}} kullanıcısına yanıt ver", "a11y/Remove edit": "Düzenlemeyi kaldır", "a11y/Remove reply": "Yanıtı kaldır", + "a11y/Replying to {{user}}": "{{user}} kullanıcısına yanıt veriliyor", + "a11y/Replying to {{user}}: {{text}}": "{{user}} kullanıcısına yanıt veriliyor: {{text}}", "a11y/Scroll to bottom": "En alta git", "a11y/Scroll to bottom, {{count}} new messages": "En alta git, {{count}} yeni mesaj", "a11y/Scroll to latest": "En son mesaja git", @@ -274,7 +286,10 @@ "a11y/Send message": "Mesaj gönder", "a11y/Sending": "Gönderiliyor", "a11y/Sent": "Gönderildi", + "a11y/Sent by you": "Sizin tarafınızdan gönderildi", "a11y/Voice message recording. Hold to record.": "Sesli mesaj kaydı. Kaydetmek için basılı tutun.", + "a11y/and {{count}} more reactions": "ve {{count}} tepki daha", + "a11y/you reacted": "siz tepki verdiniz", "a11y/{{count}} new messages": "{{count}} yeni mesaj", "a11y/Add attachment": "Add attachment", "a11y/Close attachments": "Close attachments", @@ -352,12 +367,9 @@ "size limit": "boyut sınırı", "unknown error": "bilinmeyen hata", "unsupported file type": "desteklenmeyen dosya türü", - "a11y/Activate to view results": "Sonuçları görmek için etkinleştir", - "a11y/End vote": "Oylamayı sonlandır", - "a11y/Show all options": "Tüm seçenekleri göster", - "a11y/Vote on {{option}}": "{{option}} için oy ver", "a11y/Double tap and hold to activate contextual menu": "Bağlam menüsünü etkinleştirmek için çift dokunup basılı tut", "a11y/Swipe right to go through different actions": "Farklı eylemler arasında geçiş yapmak için sağa kaydır", "a11y/Close": "Close", - "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss." + "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.", + "a11y/{{count}} unread messages": "{{count}} okunmamış mesaj" }