From 1444eaeded76fe266f97765de9e8ffb1b800d54f Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Mon, 4 May 2026 12:13:51 +0200 Subject: [PATCH 01/19] feat(a11y): add opt-in accessibility announcer, context, and hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of the accessibility plan. Foundation only; no component changes. - AccessibilityContext: opt-in config (`enabled` defaults to false). Flat `'auto' | 'always' | 'never'` enums for gesture-alternative toggles. - AccessibilityAnnouncer + useAccessibilityAnnouncer: imperative announcer via AccessibilityInfo.announceForAccessibility, with sequence/debounce so repeat messages still announce. Returns no-op when disabled. - NotificationAnnouncer: connection-state announcements (offline/online/ reconnecting). Mount once inside Channel (Phase 4a). - useIncomingMessageAnnouncements: ports stream-chat-react's hook — throttled, batched, bounded id set. Subscribes only when enabled. - a11y/ utilities: composeAccessibilityLabel, formatAccessibilityValue, mergeAccessibilityActions, useScreenReaderEnabled, useReducedMotionPreference, useResolvedModalAccessibilityProps, useAnnounceOnStateChange, useA11yLabel. - Chat.tsx wires AccessibilityProvider + AccessibilityAnnouncer into the existing provider stack. - aria/* i18n keys added to all 12 locales (English values for now; translations are a follow-up). validate-translations passes. Note: skipped the planned native handler abstraction in native.ts — AccessibilityInfo from react-native is identical on bare RN and Expo, so a wrapper handler would be unnecessary indirection. Tests: 16 new unit tests (a11yUtils, AccessibilityAnnouncer, AccessibilityContext). Refs: stream-chat-react#3146 — primitive shapes mirrored, RN-specific deviations (gesture toggles, opt-in default) documented in the plan. --- .claude/plans/2026-05-04-accessibility.md | 364 ++++++++++++++++++ package/src/a11y/__tests__/a11yUtils.test.ts | 66 ++++ package/src/a11y/a11yUtils.ts | 50 +++ package/src/a11y/hooks/useA11yLabel.ts | 19 + .../a11y/hooks/useAnnounceOnStateChange.ts | 40 ++ .../a11y/hooks/useReducedMotionPreference.ts | 38 ++ .../useResolvedModalAccessibilityProps.ts | 30 ++ .../src/a11y/hooks/useScreenReaderEnabled.ts | 44 +++ package/src/a11y/index.ts | 6 + .../Accessibility/AccessibilityAnnouncer.tsx | 78 ++++ .../Accessibility/NotificationAnnouncer.tsx | 43 +++ .../__tests__/AccessibilityAnnouncer.test.tsx | 67 ++++ .../hooks/useIncomingMessageAnnouncements.ts | 157 ++++++++ package/src/components/Accessibility/index.ts | 4 + .../useAccessibilityAnnouncer.ts | 30 ++ package/src/components/Chat/Chat.tsx | 20 +- package/src/components/index.ts | 2 + .../AccessibilityContext.tsx | 59 +++ .../__tests__/AccessibilityContext.test.tsx | 40 ++ .../contexts/accessibilityContext/index.ts | 1 + package/src/contexts/index.ts | 1 + package/src/hooks/index.ts | 1 + package/src/i18n/en.json | 151 ++++---- package/src/i18n/es.json | 151 ++++---- package/src/i18n/fr.json | 151 ++++---- package/src/i18n/he.json | 151 ++++---- package/src/i18n/hi.json | 151 ++++---- package/src/i18n/it.json | 151 ++++---- package/src/i18n/ja.json | 151 ++++---- package/src/i18n/ko.json | 151 ++++---- package/src/i18n/nl.json | 151 ++++---- package/src/i18n/pt-br.json | 151 ++++---- package/src/i18n/ru.json | 151 ++++---- package/src/i18n/tr.json | 151 ++++---- 34 files changed, 2179 insertions(+), 793 deletions(-) create mode 100644 .claude/plans/2026-05-04-accessibility.md create mode 100644 package/src/a11y/__tests__/a11yUtils.test.ts create mode 100644 package/src/a11y/a11yUtils.ts create mode 100644 package/src/a11y/hooks/useA11yLabel.ts create mode 100644 package/src/a11y/hooks/useAnnounceOnStateChange.ts create mode 100644 package/src/a11y/hooks/useReducedMotionPreference.ts create mode 100644 package/src/a11y/hooks/useResolvedModalAccessibilityProps.ts create mode 100644 package/src/a11y/hooks/useScreenReaderEnabled.ts create mode 100644 package/src/a11y/index.ts create mode 100644 package/src/components/Accessibility/AccessibilityAnnouncer.tsx create mode 100644 package/src/components/Accessibility/NotificationAnnouncer.tsx create mode 100644 package/src/components/Accessibility/__tests__/AccessibilityAnnouncer.test.tsx create mode 100644 package/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts create mode 100644 package/src/components/Accessibility/index.ts create mode 100644 package/src/components/Accessibility/useAccessibilityAnnouncer.ts create mode 100644 package/src/contexts/accessibilityContext/AccessibilityContext.tsx create mode 100644 package/src/contexts/accessibilityContext/__tests__/AccessibilityContext.test.tsx create mode 100644 package/src/contexts/accessibilityContext/index.ts diff --git a/.claude/plans/2026-05-04-accessibility.md b/.claude/plans/2026-05-04-accessibility.md new file mode 100644 index 0000000000..a1818203b8 --- /dev/null +++ b/.claude/plans/2026-05-04-accessibility.md @@ -0,0 +1,364 @@ +# Accessibility (a11y) Implementation Plan — `stream-chat-react-native` + +> **Note on plan location:** Per project convention, plans live at the repo's `.claude/plans/`. This file was created at the harness-mandated path during plan mode; once approved, copy it into `stream-chat-react-native/.claude/plans/` so it's checked in alongside the code. + +> **Aligned with [`stream-chat-react#3146`](https://github.com/GetStream/stream-chat-react/pull/3146)** ("feat(a11y): improve accessibility across dialogs, forms, menus, media, and focus flows" — merged, 194 files, +7057 / −681). This RN plan mirrors React's folder structure, primitive APIs, and i18n approach where the platforms agree, and explicitly flags the places where mobile (iOS/Android) requires different mechanisms (gestures, modal focus, no keyboard navigation, imperative announcer instead of DOM live regions). + +--- + +## Context + +The RN SDK has minimal a11y today: + +- `accessibilityLabel` is the only a11y prop in meaningful use (~73 occurrences, hardcoded English). +- Zero usage of `accessibilityRole`, `accessibilityState`, `accessibilityHint`, `accessibilityValue`, `accessibilityActions`, `accessibilityLiveRegion`, `accessibilityViewIsModal`, `onAccessibilityAction`. +- ~252 interactive surfaces and ~109 Avatar usages largely without semantic data. +- All overlays/sheets (`BottomSheetModal`, `MessageOverlayWrapper`, `AttachmentPicker`, `MessageReactionPicker`) lack modal focus-trap props. +- Critical gesture-only flows have no screen-reader/keyboard alternative: `MessageMenu` long-press, audio recorder (`Gesture.LongPress` + swipe-to-lock), `ImageGallery` (multi-gesture pan/pinch/double-tap), inline gallery long-press menu. +- Loading/empty/error indicators (`LoadingDots`, `LoadingIndicator`, `EmptyStateIndicator`, `LoadingErrorIndicator`) animate/render without `accessibilityLiveRegion`, so SR users get no signal that state is loading or failed. +- `ProgressControl/{ProgressBar,ProgressThumb,WaveProgressBar}` (used by audio attachment, audio recording preview, image-gallery video, polls) lacks `accessibilityRole="progressbar"` + `accessibilityValue`. +- `AITypingIndicatorView` ("Thinking…" / "Generating…") animates without a polite live region — AI state transitions go unannounced. +- Channel/Thread preview delivery icons (`ChannelMessagePreviewDeliveryStatus`, `ThreadMessagePreviewDeliveryStatus`) ship without labels even though `Message/MessageItemView/MessageStatus` already labels its variant. +- `Reply` / `ReplyMessageView` quoted-message previews are tappable but unlabeled and have no role. + +Goal: bring RN to parity with the React SDK's a11y baseline using the same primitive shapes, AND fill in the mobile-only gaps (gesture alternatives, modal a11y props, imperative announcer infra) so the SDK is usable with **VoiceOver (iOS)** and **TalkBack (Android)** out of the box. + +--- + +## Confirmed decisions + +1. **Translate `aria/*` keys into all 12 RN locales** — `de`, `en`, `es`, `fr`, `he`, `hi`, `it`, `ja`, `ko`, `nl`, `pt-br`, `ru`, `tr`. (`he.json` exists in RN but not React — translate that too.) Mirrors the React PR's policy. `validate-translations` enforces no empty values. +2. **Drop the `customAccessibilityLabels` override map.** Integrators override `aria/*` keys through the existing Streami18n mechanism — no new API surface. +3. **Keep the minimal `` config** — RN-specific because mobile has gesture-only flows (audio hold-to-record, gallery pinch/pan) that web doesn't. Flat config with positive `'auto' | 'always' | 'never'` enums for the gesture-alternative toggles (no nested `componentOverrides`, no negative `disable*` booleans — see "Architecture" below). Documented as an intentional deviation from React. +4. **A11y is OFF by default — integrators opt in via ``.** Keeps zero-config behavior identical to today's SDK so existing integrators see no change. Once enabled, sensible defaults take over (auto-adapt to SR, announce new messages, etc.). The Phase 5 Reassure benchmark will measure the cost of `enabled: true` so we can confidently flip the default to `true` in a future release. + +--- + +## Mapping React (web) → React Native + +| React (web) | React Native | +|---|---| +| `aria-label` | `accessibilityLabel` | +| `aria-labelledby` / `aria-describedby` | (no equivalent — fold into a single composed `accessibilityLabel`) | +| `role="dialog"` + `aria-modal="true"` | `accessibilityViewIsModal={true}` (iOS) + `importantForAccessibility="no-hide-descendants"` on background siblings (Android) | +| `aria-live="polite"` + DOM region | `AccessibilityInfo.announceForAccessibility` (iOS+Android) AND `accessibilityLiveRegion="polite"` on hidden View (Android backup) | +| `tabIndex` + Enter/Space handler | `accessibilityRole="button"` + `onAccessibilityAction` (rotor on iOS, local context menu on Android) | +| `prefers-reduced-motion` (CSS+JS) | `AccessibilityInfo.isReduceMotionEnabled()` + event listener | +| `aria-hidden="true"` | `accessibilityElementsHidden={true}` (iOS) + `importantForAccessibility="no-hide-descendants"` (Android) | +| `aria-selected` / `aria-checked` | `accessibilityState={{ selected, checked, disabled, busy, expanded }}` | +| `focus()` / focus restore | `AccessibilityInfo.setAccessibilityFocus(reactTag)` via `findNodeHandle` | +| `jest-axe` | `@testing-library/react-native` semantic queries (`getByRole`, `getByLabelText`); no axe equivalent for RN | +| `` | Not needed — RN announcer is imperative, no hidden DOM node required | +| `` | Not applicable — mobile has no Tab key | +| Roving focus (`a11yUtils.ts`) | Not applicable — accessibility tree order is implicit; rely on view hierarchy | + +--- + +## Architecture (mirrors React folder shape) + +### `package/src/a11y/` — utilities + low-level hooks + +Mirrors `stream-chat-react/src/a11y/`. Smaller in RN because keyboard helpers don't apply. + +- `a11yUtils.ts` — `composeAccessibilityLabel(...parts)`, `formatAccessibilityValue({min, max, now})`, `mergeAccessibilityActions(...)`. (No roving-focus helper — N/A on RN.) +- `hooks/useResolvedModalAccessibilityProps.ts` — returns the `{ accessibilityViewIsModal, importantForAccessibility, accessibilityRole }` triple correctly for the active platform; equivalent of React's `useResolvedModalAriaProps`. +- `hooks/useScreenReaderEnabled.ts` — subscribes to `AccessibilityInfo.screenReaderChanged`. RN-specific (web doesn't expose this). +- `hooks/useReducedMotionPreference.ts` — same name as React's hook; subscribes to `AccessibilityInfo.reduceMotionChanged`. +- `__tests__/a11yUtils.test.ts` — parity unit tests. + +### `package/src/components/Accessibility/` — runtime announcement infra + +Mirrors `stream-chat-react/src/components/Accessibility/`. Same component graph, mobile implementations swap DOM live regions for `AccessibilityInfo.announceForAccessibility`. + +- `AccessibilityAnnouncer.tsx` — Provider equivalent of React's `AriaLiveRegion`. Exposes a queue with two priorities (`polite` / `assertive`); flushes through `AccessibilityInfo.announceForAccessibility` on iOS, and through a hidden Android `` (rendered absolutely off-screen) for TalkBack reliability. Uses the same sequence/timeout pattern from `AriaLiveRegion.tsx` so repeat messages still re-announce. +- `useAccessibilityAnnouncer.ts` — `useAccessibilityAnnouncer()` returns `(message, priority?) => void`. Same shape as React's `useAriaLiveAnnouncer`. +- `NotificationAnnouncer.tsx` — same component name and same `buildNotificationAnnouncement` / `notificationFilter` props as React. Source of notifications differs: in RN we wire to `useChannelContext().error` and `useChatContext().connectionState` events (no shared notifications queue exists today; building one is out of scope for this plan, so we adapt the source). +- `hooks/useIncomingMessageAnnouncements.ts` — direct port of React's hook: throttles to 1 announcement / sec, batches: 1 message → "New message from {{user}}", N>1 → "{{count}} new messages". Same params (`channel`, `ownUserId`, `activeThreadId`, `threadList`). +- `hooks/__tests__/useIncomingMessageAnnouncements.test.tsx` — parity tests. +- `__tests__/AccessibilityAnnouncer.test.tsx` — sequence + priority + Android-fallback tests. +- `index.ts` — barrel. + +### `` integration + +Mount `` inside the existing provider stack (between `ThemeProvider` and `ChannelsStateProvider`). Mount `` once inside `Channel` so it can subscribe to the active channel's errors. Equivalent to where the React PR mounts `AriaLiveRegion` + `NotificationAnnouncer` in the Chat root. + +### Native handler — RN-specific + +Web has direct DOM access; RN needs platform abstraction. Extend [package/src/native.ts](package/src/native.ts) the same way `Audio`, `Sound`, etc. are registered: + +```ts +type AccessibilityInfoHandlers = { + isScreenReaderEnabled: () => Promise; + isReduceMotionEnabled: () => Promise; + announceForAccessibility: (message: string) => void; + setAccessibilityFocus: (reactTag: number) => void; + addEventListener: ( + eventName: 'screenReaderChanged' | 'reduceMotionChanged', + handler: (enabled: boolean) => void, + ) => { remove: () => void }; +}; +``` + +Both `native-package/` and `expo-package/` register implementations against React Native's built-in `AccessibilityInfo` (identical on both — no platform divergence inside the handler). Falls back to no-op stubs if not registered, so the SDK degrades gracefully (matching the existing `fail()` pattern in [native.ts](package/src/native.ts)). + +### `` — minimal RN-only config (deviation from React) + +React did not add a Chat-level config. RN needs one because: +- Some gestures (audio recorder hold-to-record, gallery pinch/pan) have no inherent a11y; integrators may want to opt out of the SDK's automatic alternatives if they ship their own. +- Integrators may need to disable announcement behavior to avoid duplicate announcers when embedding the SDK in a host app that already announces. + +Type (flat, positive enums — no nesting, no `disable*` flags): + +```ts +/** Tri-state for gesture-alternative toggles. */ +export type A11yMode = 'auto' | 'always' | 'never'; + +export type AccessibilityConfig = { + /** Master toggle. Default FALSE — integrators must opt in. When false, the SDK behaves exactly as it does today; no a11y attributes are added, no announcer mounts, no listeners attached. */ + enabled?: boolean; + + /** For testing — force "screen reader on" UI even when no SR is active. Default false. */ + forceScreenReaderMode?: boolean; + + /** Announce new messages via the announcer. Default true (when `enabled`). */ + announceNewMessages?: boolean; + /** Announce typing indicator. Default false (noisy on mobile). */ + announceTypingIndicator?: boolean; + /** Announce connection state (offline/online). Default true. */ + announceConnectionState?: boolean; + + // RN-specific gesture-alternative toggles. 'auto' = swap UI when SR is on; + // 'always' = show accessible variant for everyone; 'never' = SDK never swaps + // (integrator handles it). All default to 'auto'. + audioRecorderTapMode?: A11yMode; + imageGalleryScreenReaderMode?: A11yMode; + messageActionsTrigger?: 'long-press' | 'auto' | 'always-button'; +}; +``` + +Naming note: the previous draft had a nested `componentOverrides` field. Dropped — it collided with the existing `WithComponents` override pattern (commit `15dd5e10d`) and the negative `disable*` flags were hard to reason about. If an integrator replaces a component entirely via `WithComponents`, the SDK's a11y code for that component never runs anyway. The flat enums above only matter when the integrator keeps the SDK's component but wants different gesture behavior. + +Lives in a small `AccessibilityContext` under `package/src/contexts/accessibilityContext/AccessibilityContext.tsx` following the [`ChatConfigContext` template](package/src/contexts/chatConfigContext/ChatConfigContext.tsx). Default value is `{ enabled: false }` — every other field is ignored unless `enabled` is true. Opt-in is a single flag flip: ``. + +When `enabled: false`, the implementation must short-circuit cleanly: +- No `` mount; `useAccessibilityAnnouncer()` returns a noop. +- No `` mount. +- No `useIncomingMessageAnnouncements` subscription on `channel.on('message.new')`. +- No `AccessibilityInfo` event listeners. +- Component-level a11y props (`accessibilityRole`, `accessibilityState`, etc.) still render — these are passed to native views and consulted only by VO/TalkBack when active, so they cost essentially nothing for sighted users. **Exception: `accessibilityLabel` strings composed via `t('aria/...')`.** Skip the `t()` call when `enabled: false` to avoid 1000 i18next lookups in a busy `MessageList`. A small helper hook `useA11yLabel(key, params)` returns `undefined` when disabled and the translated string when enabled — components pass its return value straight to `accessibilityLabel`. + +### i18n — `aria/*` namespace (matches React) + +React used `t('aria/...')` keys for parity across SDKs. RN adopts the same prefix even though "ARIA" is web-specific — the value is cross-SDK consistency for translators and integrator docs. Add keys to all 12 RN locales: `de.json`, `en.json`, `es.json`, `fr.json`, `hi.json`, `it.json`, `ja.json`, `ko.json`, `nl.json`, `pt-br.json`, `ru.json`, `tr.json`. (`he.json` exists in RN but not React — translate too.) Run `yarn build-translations` to keep the i18next-cli sync intact, then `yarn lint` to pass `validate-translations` (no empty values). + +Example shared keys (from React PR — adopt verbatim where the string is platform-neutral): +``` +aria/Avatar of {{name}} +aria/{{count}} new messages +aria/New message from {{user}} +aria/Open message actions +aria/Send message +aria/Voice message recording. Hold to record. +aria/Reaction {{emoji}} by {{count}} users +aria/Reply to {{user}} +Anonymous ← shared with React +``` + +--- + +## Phased Implementation + +### Phase 1 — Foundation _(1 commit)_ + +Mirrors React PR's "Screen reader foundations" section. + +1. Create `package/src/a11y/` (utils + hooks listed above). +2. Create `package/src/components/Accessibility/` with `AccessibilityAnnouncer`, `useAccessibilityAnnouncer`, `NotificationAnnouncer`, `useIncomingMessageAnnouncements`, `index.ts`. +3. Create `package/src/contexts/accessibilityContext/AccessibilityContext.tsx` (provider + `useAccessibilityContext()`), following the [`ChatConfigContext`](package/src/contexts/chatConfigContext/ChatConfigContext.tsx) template. +4. Add `accessibility?: AccessibilityConfig` prop to `ChatProps` in [package/src/components/Chat/Chat.tsx](package/src/components/Chat/Chat.tsx). Mount `` and `` inside the existing provider stack. +5. Mount `` once inside `Channel` so it can subscribe to per-channel errors. +6. Extend [package/src/native.ts](package/src/native.ts) with `AccessibilityInfo` handlers + `isAccessibilityInfoAvailable()` check (mirror `isAudioRecorderAvailable()` style). +7. Register handlers in `package/native-package/src/handlers/AccessibilityInfoHandler.ts` and `package/expo-package/src/handlers/AccessibilityInfoHandler.ts` — both wrap RN's `AccessibilityInfo`. +8. Add `aria/*` keys to all 12 locales; run `yarn build-translations`. +9. Update [package/src/contexts/index.ts](package/src/contexts/index.ts) and `package/src/index.ts` to export `AccessibilityContext`, `AccessibilityConfig`, `useAccessibilityAnnouncer`, hooks. (Same exports React added in `src/components/Accessibility/index.ts` and `src/components/index.ts`.) +10. Add unit tests with parity to React's: `AriaLiveRegion.test.tsx` → `AccessibilityAnnouncer.test.tsx`, `useIncomingMessageAnnouncements.test.tsx` → port verbatim. + +**Done when:** integrators can consume `useAccessibilityAnnouncer()` and `useScreenReaderEnabled()`, and incoming-message announcements fire on real devices. + +### Phase 2 — Base UI primitives _(1 commit)_ + +Mirrors React PR's `Avatar`, `BaseImage`, `Button/PlayButton`, `Form/{TextInput,SwitchField,Dropdown,NumericInput}`, `Icons/BaseIcon`, `Dialog/{Alert,Prompt,Viewer,Callout,ContextMenu,DialogPortal,DialogAnchor}`. + +> **Note on folder split:** primitives live in BOTH `package/src/components/ui/` (low-level: Avatar, Badge, Button, Input, GiphyChip, SpeedSettingsButton, VideoPlayIndicator) AND `package/src/components/UIComponents/` (composite: BottomSheetModal, ImageBackground, PortalWhileClosingView, SwipableWrapper, Spinner, SafeAreaViewWrapper). Touch both during this phase. + +1. **Avatar** — `package/src/components/Avatar/Avatar.tsx`, `UserAvatar.tsx`, `ChannelAvatar.tsx`, `AvatarStack.tsx`, plus the lower-level `package/src/components/ui/Avatar/`. Add `accessibilityRole="image"`, `accessibilityLabel={t('aria/Avatar of {{name}}', {name})}`. Allow integrators to override the label via prop. (React's `Avatar.tsx` made the same change; mirror.) +2. **Button / IconButton** (`package/src/components/ui/Button.tsx`, plus icon-button equivalents): `accessibilityRole="button"`, propagate `accessibilityLabel`/`accessibilityHint`/`accessibilityState={{disabled, busy}}` to `Pressable`. Hit-slop expanded to 44×44 (Apple HIG) when smaller. Mirror React's `BaseIcon.tsx` change to mark decorative SVGs with `accessibilityElementsHidden`. +3. **Input** (`package/src/components/ui/Input.tsx`): wire `accessibilityLabel`, `accessibilityHint`, `accessibilityState={{ disabled, selected }}`. Validation/error state uses the announcer (RN's substitute for `aria-describedby`). Same shape as React's `TextInput.tsx` and `NumericInput.tsx`. +4. **Switch** (already a native control on RN — verify it surfaces label/state): mirror React's `SwitchField.tsx` semantics (`accessibilityRole="switch"`, `accessibilityState={{ checked }}`). +5. **Dropdown / autocomplete picker** equivalents — `package/src/components/AutoCompleteInput/`: `accessibilityRole="list"` on container, items get role + `accessibilityState={{ selected }}`. Same intent as React's `Dropdown.tsx` + roving focus, minus the keyboard nav. +6. **Modal / overlay primitives** — `package/src/components/UIComponents/BottomSheetModal.tsx`, `BottomSheetCompatibility/{BottomSheet,BottomSheetFlatList,BottomSheetTouchableOpacity}.tsx`, `StreamBottomSheetModalFlatList.tsx`. Use `useResolvedModalAccessibilityProps` to apply `accessibilityViewIsModal` + `importantForAccessibility`. Set initial focus to the modal title via `setAccessibilityFocus`. Restore focus to invoking trigger on close. Account for the dynamic snap-points behavior added in `7a7f927ae` — re-issue `setAccessibilityFocus` after resize so VO/TalkBack keeps focus inside the sheet. Equivalent of React's `Alert`/`Prompt`/`Viewer`/`DialogPortal` work. +7. **Indicators** — `package/src/components/Indicators/{LoadingDots,LoadingDot,LoadingIndicator,LoadingErrorIndicator,EmptyStateIndicator}.tsx`. `LoadingDots`/`LoadingIndicator`: wrap in a hidden View with `accessibilityLiveRegion="polite"` (Android) and announce `t('aria/Loading…')` once via `useAccessibilityAnnouncer` on mount; suppress repeats. `EmptyStateIndicator`/`LoadingErrorIndicator`: static `accessibilityLabel` + `accessibilityRole="text"` (or `"alert"` for error variant). Hide the visual dots/spinner from AT (`accessibilityElementsHidden={true}`) so the announcement isn't duplicated. +8. **ProgressControl** — `package/src/components/ProgressControl/{ProgressBar,ProgressControl,ProgressThumb,WaveProgressBar,StableDurationLabel}.tsx`. Add `accessibilityRole="progressbar"` + `accessibilityValue={{ min, max, now, text }}`. When the consumer is interactive (audio scrub, gallery video, poll-result reveal), expose `accessibilityActions: [{name:'increment'}, {name:'decrement'}]` so rotor users can seek. `ProgressThumb` becomes `accessibilityRole="adjustable"` when draggable. Single shared component covers AudioAttachment, AudioRecordingPreview, ImageGalleryVideoControl, and PollOption — fix once, propagate everywhere. + +**Done when:** every Avatar, Button, Input, IconButton, modal, dropdown, indicator, and progress control in the SDK has correct semantics and modal focus-trapping works on both platforms. + +### Phase 3 — Critical-path components _(2 commits)_ + +#### 3a. Message + MessageMenu + Reactions + +Mirrors React PR's `Message/MessageUI.tsx`, `Message/MessageText.tsx`, `MessageActions/*`, `Reactions/{MessageReactions,ReactionSelector,MessageReactionsDetail}`. + +- **`package/src/components/Message/Message.tsx`** — container `accessibilityRole="article"`. Composed `accessibilityLabel` mirroring React's pattern (sender + timestamp + text + reactions summary, capped at top-3 reactions). Long-press → `accessibilityActions` exposed to the rotor (iOS) and Android local context menu: `[{ name:'activate', label:t('aria/Open message actions') }, { name:'react' }, { name:'reply' }, { name:'copy' }]`. Visibility of the alternative "More actions" button is driven by `accessibility.messageActionsTrigger`: `'long-press'` → hidden; `'auto'` (default) → shown when SR is on; `'always-button'` → shown for everyone. +- **`package/src/components/MessageMenu/MessageActionList.tsx`** — wrapper `accessibilityRole="menu"`, items `accessibilityRole="menuitem"`. Same as React's `MessageActions.defaults.tsx`. +- **`package/src/components/MessageMenu/MessageReactionPicker.tsx`** — `accessibilityRole="grid"` on emoji list, each emoji `accessibilityLabel` + `accessibilityState={{ selected }}`. Same shape as React's `ReactionSelector.tsx`. +- **`package/src/components/Reaction/ReactionList*.tsx`** — pills get `accessibilityRole="button"`, `accessibilityLabel={t('aria/Reaction {{emoji}} by {{count}} users', ...)}`, `accessibilityState={{ selected: isOwnReaction }}`. Same as React's `MessageReactions.tsx`. +- **`package/src/components/Message/MessageOverlayWrapper.tsx`** — `useResolvedModalAccessibilityProps`, focus management on open. +- **`package/src/components/Reply/{Reply,ReplyMessageView}.tsx`** — quoted-message preview. `accessibilityRole="button"` when tappable (jump-to-original), composed `accessibilityLabel` of form `t('aria/Reply to {{user}}: {{preview}}')`. The preview's inner avatar/text re-uses the labels from Phase 2. +- **`package/src/components/Message/MessageItemView/MessageStatus.tsx`** — already has labels for `Read`/`Delivered`/`Sending`/`Sent`. Migrate the strings to `aria/*` keys for parity. + +#### 3b. MessageList + MessageInput + AudioRecorder + +Mirrors React PR's `MessageList/{MessageList,VirtualizedMessageList,UnreadMessagesNotification,ScrollToLatestMessageButton}`, `MessageComposer/*`, `MediaRecorder/AudioRecorder/*`. + +- **`package/src/components/MessageList/MessageList.tsx`** / `MessageFlashList.tsx` — wrap announcement of new messages through `useIncomingMessageAnnouncements({ channel, ownUserId })`. Direct port of React's hook usage. Gated on `accessibility.announceNewMessages`. +- **`package/src/components/MessageList/InlineUnreadIndicator.tsx`** / `ScrollToBottomButton.tsx` — labels via `t()`. Same as React's `UnreadMessagesNotification.tsx` / `ScrollToLatestMessageButton.tsx`. +- **`package/src/components/MessageList/TypingIndicator.tsx`** — when `announceTypingIndicator` is true, debounced announcement of "X is typing". (React enabled by default; RN defaults to false because TalkBack/VoiceOver chatter on mobile is more disruptive.) +- **`package/src/components/MessageInput/MessageComposer.tsx`** — TextInput labels + hint, send/attach/audio buttons labeled. Validation routes through announcer with priority `assertive`. +- **`package/src/components/MessageInput/AudioRecorder/AudioRecordingButton.tsx`** — RN-specific (no React analog because web doesn't have hold-to-record). Behavior driven by `accessibility.audioRecorderTapMode`: `'auto'` (default) → swap to tap-toggle when `useScreenReaderEnabled()` is true; `'always'` → tap-toggle for everyone; `'never'` → keep `Gesture.LongPress` always. In tap mode: tap → start, tap → stop, tap → send. Lock-by-swipe replaced with explicit "Lock recording" button. Each state announces through the announcer ("Recording, {duration}", "Recording locked", "Send recording", "Cancel recording"). + +### Phase 4 — Secondary components _(2 commits)_ + +#### 4a. Channels & threads + +Mirrors React PR's `ChannelListItem/{ChannelListItemUI,ChannelListItemActionButtons*}`, `Threads/ThreadList/*`, `TypingIndicator/*`. + +- **`package/src/components/ChannelList/ChannelList.tsx`** + `ChannelPreview/ChannelPreviewView.tsx` — items `accessibilityRole="button"`, composed label. Swipe actions get `accessibilityActions`. +- **`package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx`** + `package/src/components/ThreadList/ThreadMessagePreviewDeliveryStatus.tsx` — port the labeled-icon pattern from `MessageItemView/MessageStatus.tsx` so preview rows announce delivery state to SR users. +- **`package/src/components/Thread/Thread.tsx`** + `ThreadList/ThreadList.tsx` — reply count → `t('aria/{{count}} reply', { count })`. Unread banner uses the announcer. +- **`package/src/components/Channel/Channel.tsx`** — connection state changes (offline → online) routed through ``. `useIncomingMessageAnnouncements` lifts here. +- **`package/src/components/AITypingIndicatorView/AITypingIndicatorView.tsx`** — wrap in `accessibilityLiveRegion="polite"`. On state transitions (`Thinking…` → `Generating…` → idle), call `useAccessibilityAnnouncer().announce(t('aria/AI is {{state}}', {state}))` with debounce so transitions don't spam VO/TalkBack. Hide the animated dots from AT (`accessibilityElementsHidden`) so the announcement is the only signal. + +#### 4b. Media, attachments, polls, autocomplete + +Mirrors React PR's `Attachment/*`, `AudioPlayback/*`, `Gallery/*`, `Poll/*`, `Search/*`, `MessageComposer/AttachmentSelector/*`, `TextareaComposer/SuggestionList/*`. + +- **`package/src/components/Attachment/ImageGallery/ImageGallery.tsx`** + `useImageGalleryGestures.tsx` — RN-specific (web has no equivalent). Behavior driven by `accessibility.imageGalleryScreenReaderMode`: `'auto'` (default) → swap when SR is on; `'always'` → swap for everyone; `'never'` → never swap. In swap mode: hide the gesture surface (`accessibilityElementsHidden={true}`) and render a tap-driven control set (Previous, Next, Zoom in, Zoom out, Close). Pinch/pan/double-tap kept for sighted users; `accessibilityActions` (Zoom, Reset) exposed for rotor users regardless of mode. +- **`package/src/components/Attachment/Gallery.tsx`** — inline thumbnails: button role + label. Long-press menu trigger gets a visible "More" button when SR is on. +- **`package/src/components/Attachment/AudioAttachment.tsx`** — match React's `ProgressBar.tsx` + `progressBarA11y.ts`: `accessibilityValue={{ min:0, max:duration, now:currentTime, text:"{currentTime} of {duration}" }}` on the seek bar, `accessibilityActions: [{ name:'increment' }, { name:'decrement' }]` for rotor seek. Play/pause `accessibilityState={{ selected: isPlaying }}`. +- **`package/src/components/AttachmentPicker/AttachmentPicker.tsx`** — `useResolvedModalAccessibilityProps`. Selectable photos rendered as a grid with `accessibilityRole="image"` items + `accessibilityState={{ selected }}`. Mirrors React's `AttachmentSelector.tsx`. +- **`package/src/components/Poll/Poll.tsx`** + `PollOption.tsx` + `PollResults.tsx` — single-select: `accessibilityRole="radio"`. Multi-select: `"checkbox"`. `accessibilityState={{ selected, checked }}`. Result animations announce winners via announcer when poll closes. Mirrors React's `PollOptionSelector.tsx` + `PollResults/*`. +- **`package/src/components/AutoCompleteInput`** — suggestion list `accessibilityRole="list"`; items `accessibilityRole="button"`. TextInput exposes `accessibilityHint` describing trigger characters (`@`, `/`). Mirrors React's `SuggestionList.tsx`. + +### Phase 5 — Testing, lint, docs, AI maintenance skill _(1 commit)_ + +Mirrors React PR's testing additions, AI skill (`.cursor/skills/accessibility/SKILL.md`), reduced-motion CSS, and example-app skip nav (the last is N/A on mobile). + +1. **Test patterns** — add helpers under `package/src/mock-builders/accessibility/` — `expectAccessibleButton(node, {label, role, state})`, `mockScreenReaderEnabled(boolean)`. Add a11y assertions to existing test suites for Message, MessageList, ChannelPreview, BottomSheetModal as exemplars (parity with React's targeted suites). Full coverage tracked as follow-up. +2. **Lint rules** — extend [package/eslint.config.mjs](package/eslint.config.mjs) with `eslint-plugin-react-native-a11y`: warn level for missing labels on `Pressable`/`TouchableOpacity`, error level for icon-only buttons. Set `--max-warnings 0` so it must pass before merge. +3. **Integration smoke test** — under `examples/SampleApp/`, boot Chat with `accessibility={{ enabled: true, forceScreenReaderMode: true }}` and verify AudioRecorder, ImageGallery, and Message render their accessible variants. +4. **Reassure perf benchmark** — add a Reassure (`reassure` npm package) test that renders a 1000-row `MessageList` twice: once with `accessibility={{ enabled: false }}` (today's behavior), once with `accessibility={{ enabled: true }}`. Assert: render time delta <5%, re-render delta <2%. Numbers feed the future "flip default to `true`" decision. +5. **AI maintenance skill** — `.claude/skills/accessibility/SKILL.md` (RN-equivalent of React's [`.cursor/skills/accessibility/SKILL.md`](https://github.com/GetStream/stream-chat-react/blob/master/.cursor/skills/accessibility/SKILL.md)). Same structure, RN tool names: native semantics first (`Pressable`, `TextInput`, `Switch`, `Image`); use `accessibilityRole` only when native semantics can't represent the widget; never hardcode English (use `t('aria/...')`); decorative visuals get `accessibilityElementsHidden`; modals use `useResolvedModalAccessibilityProps`; live updates use `useAccessibilityAnnouncer`; tests use `@testing-library/react-native` semantic queries. Keep React's "Common mistakes to avoid" section, RN-adapted. +6. **Documentation** — add `package/ai-docs/accessibility.md` (rename block uses **bullets, not tables** per repo convention). Cover: `accessibility` prop schema (with the **opt-in** call-out front and center), all `aria/*` i18n keys with default English, integrator override path via Streami18n, the `A11yMode` enum (`auto`/`always`/`never`) for gesture toggles, platform-specific notes (TalkBack vs VoiceOver behaviors, Android `accessibilityLiveRegion`, iOS `setAccessibilityFocus` timing), and the Reassure benchmark numbers so integrators can predict the cost of `enabled: true`. + +--- + +## Critical Files + +### New files (mirroring React structure) +- `package/src/a11y/a11yUtils.ts` +- `package/src/a11y/__tests__/a11yUtils.test.ts` +- `package/src/a11y/hooks/useResolvedModalAccessibilityProps.ts` +- `package/src/a11y/hooks/useScreenReaderEnabled.ts` +- `package/src/a11y/hooks/useReducedMotionPreference.ts` +- `package/src/components/Accessibility/AccessibilityAnnouncer.tsx` +- `package/src/components/Accessibility/useAccessibilityAnnouncer.ts` +- `package/src/components/Accessibility/NotificationAnnouncer.tsx` +- `package/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts` +- `package/src/components/Accessibility/hooks/__tests__/useIncomingMessageAnnouncements.test.tsx` +- `package/src/components/Accessibility/__tests__/AccessibilityAnnouncer.test.tsx` +- `package/src/components/Accessibility/__tests__/NotificationAnnouncer.test.tsx` +- `package/src/components/Accessibility/index.ts` +- `package/src/contexts/accessibilityContext/AccessibilityContext.tsx` +- `package/src/contexts/accessibilityContext/index.ts` +- `package/src/a11y/hooks/useAnnounceOnStateChange.ts` — small helper used by `AITypingIndicatorView` and `Indicators` to debounce + de-duplicate live-region announcements +- `package/src/a11y/hooks/useA11yLabel.ts` — returns `t('aria/...')` when the context is `enabled: true`, or `undefined` when disabled. Components pass its return value straight to `accessibilityLabel` so the i18n lookup is skipped on hot list paths in the disabled-default state. +- `package/src/__tests__/perf/AccessibilityCost.reassure.ts` — Reassure benchmark for `MessageList` with a11y on vs. off +- `package/native-package/src/handlers/AccessibilityInfoHandler.ts` +- `package/expo-package/src/handlers/AccessibilityInfoHandler.ts` +- `package/src/mock-builders/accessibility/index.ts` +- `package/ai-docs/accessibility.md` +- `.claude/skills/accessibility/SKILL.md` + +### Modified files +- [package/src/components/Chat/Chat.tsx](package/src/components/Chat/Chat.tsx) — add `accessibility` prop, mount `` + `` +- [package/src/components/Channel/Channel.tsx](package/src/components/Channel/Channel.tsx) — mount ``, lift `useIncomingMessageAnnouncements` +- [package/src/native.ts](package/src/native.ts) — register `AccessibilityInfo` handler type, add `isAccessibilityInfoAvailable()` +- [package/src/index.ts](package/src/index.ts) — public exports +- [package/src/contexts/index.ts](package/src/contexts/index.ts) — context export +- All 12 locale JSONs in [package/src/i18n/](package/src/i18n/) — `aria/*` keys +- [package/eslint.config.mjs](package/eslint.config.mjs) — a11y lint rules +- ~50 component files listed in Phases 2–4 (AITypingIndicatorView, Indicators/*, ProgressControl/*, Reply/*, ChannelPreview/ChannelMessagePreviewDeliveryStatus, ThreadList/ThreadMessagePreviewDeliveryStatus, the `ui/` primitives, and the `BottomSheetCompatibility/*` wrappers added by the post-survey audit) + +### Reused (template) files +- [package/src/contexts/chatConfigContext/ChatConfigContext.tsx](package/src/contexts/chatConfigContext/ChatConfigContext.tsx) — context boilerplate template +- [package/src/contexts/translationContext/TranslationContext.tsx](package/src/contexts/translationContext/TranslationContext.tsx) — `t()` access pattern +- `useTranslationContext()`, `useStreami18n` — for resolving and overriding `aria/*` keys +- `registerNativeHandlers` in [package/src/native.ts](package/src/native.ts) — extension pattern for new handler + +### Reference files in stream-chat-react (port verbatim) +- [`src/components/Accessibility/AriaLiveRegion.tsx`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Accessibility/AriaLiveRegion.tsx) → `AccessibilityAnnouncer.tsx` +- [`src/components/Accessibility/useAriaLiveAnnouncer.ts`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Accessibility/useAriaLiveAnnouncer.ts) → `useAccessibilityAnnouncer.ts` +- [`src/components/Accessibility/NotificationAnnouncer.tsx`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Accessibility/NotificationAnnouncer.tsx) → `NotificationAnnouncer.tsx` (notification source adapted) +- [`src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts) → ported verbatim +- [`.cursor/skills/accessibility/SKILL.md`](https://github.com/GetStream/stream-chat-react/blob/master/.cursor/skills/accessibility/SKILL.md) → `.claude/skills/accessibility/SKILL.md` with RN substitutions + +--- + +## Verification + +The plan ships as a single PR composed of **7 conventional commits** (one per phase, with Phase 3 and Phase 4 split in two). Each commit is independently reviewable and individually verifiable; the whole PR is the unit of merge. Suggested commit messages: + +- `feat(a11y): add opt-in accessibility announcer, context, and native handler` (Phase 1) +- `feat(a11y): add accessibility props to base UI primitives` (Phase 2) +- `feat(a11y): add accessible message actions and reactions` (Phase 3a) +- `feat(a11y): add accessible message list, composer, and audio recorder` (Phase 3b) +- `feat(a11y): add accessibility to channel list and threads` (Phase 4a) +- `feat(a11y): add accessibility to media, polls, and autocomplete` (Phase 4b) +- `chore(a11y): add lint rules, perf benchmark, docs, and maintenance skill` (Phase 5) + +End-to-end checks (run before merge, plus per-commit smoke checks during development): + +1. **Unit tests** — `cd package && yarn test:unit`. New hooks/components covered with parity to React's tests; `mockScreenReaderEnabled` helper validated. +2. **Lint** — `cd package && yarn lint` passes with `--max-warnings 0` plus new a11y rules. +3. **Type-check / build** — `cd package && yarn build`. Exported types (`AccessibilityConfig`, `AriaLivePriority`, etc.) compile cleanly. +4. **Translation validation** — `yarn lint` runs `validate-translations` — no empty `aria/*` keys in any of 12 locales. +5. **Manual SampleApp on real devices** for **both platforms**: + - **iOS** (Settings → Accessibility → VoiceOver): + - Send/receive message — announced via `useIncomingMessageAnnouncements`. + - Long-press alternative → Message actions menu opens, navigable, selectable. + - Audio recorder → tap-to-record mode active, all states announced. + - Image gallery → tap-driven controls, swipe-by-rotor works. + - Modals (`BottomSheetModal`, `AttachmentPicker`) → focus trapped (`accessibilityViewIsModal`), dismissable via VO escape gesture. + - **Android** (Settings → Accessibility → TalkBack): + - Same flows; verify `accessibilityLiveRegion` triggers on new messages. + - Verify `accessibilityActions` surface in TalkBack's local context menu. +6. **Reduced motion** — enable in OS settings; verify TypingIndicator dots, AudioRecorder waveform, ImageGallery transitions reduce or disable animation via `useReducedMotionPreference`. +7. **No-regression for sighted users** — confirm visual UI is unchanged when SR is off (no new buttons appear, no animation changes outside reduced-motion). +8. **Cross-SDK API parity check** — verify `useAccessibilityAnnouncer().announce('hi')` and the React `useAriaLiveAnnouncer()('hi')` have identical call shape; same for `useIncomingMessageAnnouncements` params. +9. **RTL smoke** — switch device language to Hebrew or Arabic (RN's `I18nManager.isRTL` becomes true; `RTLComponents/WritingDirectionAwareText` flips). Verify VO/TalkBack reads composed `aria/*` strings with parameters (`{{name}}`, `{{count}}`, `{{user}}`) in the correct logical order — interpolation values must not appear visually-flipped inside a labeled control. +10. **KeyboardCompatibleView focus** — open the composer with VO/TalkBack on, send a message, verify focus does NOT escape to a stale element when `KeyboardCompatibleView`/`KeyboardControllerAvoidingView` re-lays out. If it does, defer `setAccessibilityFocus` calls behind `requestAnimationFrame` (Android) / `InteractionManager.runAfterInteractions` (iOS). +11. **Component overrides inherit a11y props** — confirm that the recently introduced `WithComponents` provider (`15dd5e10d`) threads a11y props correctly when integrators replace `Message`/`MessageList`/etc.; add a regression test that renders the SDK with a custom `Message` override and asserts the rendered tree still carries `accessibilityRole="article"` + `accessibilityLabel`. + +--- + +## Out of scope (explicit non-goals) + +- **Skip navigation links** — applicable only to web (no Tab key on mobile). +- **Roving focus utilities** — applicable only to web (`a11yUtils.ts` keyboard helpers). +- **`` component** — RN announcer is imperative; no hidden DOM node needed. +- **Keyboard navigation patterns** (arrow keys, Enter/Space) — not applicable on mobile devices; rotor and `accessibilityActions` cover the equivalent UX. +- **`jest-axe` parity** — no equivalent for RN; semantic queries via `@testing-library/react-native` are the closest substitute. +- **Building a unified Notifications queue in RN** — React has `useNotifications`; RN does not. `NotificationAnnouncer` adapts to existing `useChatContext` errors and `useChannelContext` errors. A full notification queue is a separate plan. +- **Web a11y semantics** — `stream-chat-react-native` runs on RN-Web but the scope is iOS + Android; web is best-effort. +- **Dynamic Type / font scaling beyond RN's defaults** — separate plan if integrators request. +- **Auditing every existing test for a11y queries** — only exemplar suites updated in Phase 5. diff --git a/package/src/a11y/__tests__/a11yUtils.test.ts b/package/src/a11y/__tests__/a11yUtils.test.ts new file mode 100644 index 0000000000..f91620f264 --- /dev/null +++ b/package/src/a11y/__tests__/a11yUtils.test.ts @@ -0,0 +1,66 @@ +import { + composeAccessibilityLabel, + formatAccessibilityValue, + mergeAccessibilityActions, +} from '../a11yUtils'; + +describe('composeAccessibilityLabel', () => { + it('joins non-empty parts with comma+space', () => { + expect(composeAccessibilityLabel('Alice', '12:34', 'Hello')).toBe('Alice, 12:34, Hello'); + }); + + it('drops empty strings, null, undefined, false', () => { + expect(composeAccessibilityLabel('A', '', null, undefined, false, 'B')).toBe('A, B'); + }); + + it('returns empty string when nothing to join', () => { + expect(composeAccessibilityLabel(null, undefined, '')).toBe(''); + }); +}); + +describe('formatAccessibilityValue', () => { + it('clamps now between min and max', () => { + expect(formatAccessibilityValue({ max: 100, now: 200 })).toEqual({ max: 100, min: 0, now: 100 }); + expect(formatAccessibilityValue({ max: 100, min: 10, now: 5 })).toEqual({ + max: 100, + min: 10, + now: 10, + }); + }); + + it('attaches optional text', () => { + expect(formatAccessibilityValue({ max: 60, now: 30, text: '00:30 of 01:00' })).toEqual({ + max: 60, + min: 0, + now: 30, + text: '00:30 of 01:00', + }); + }); + + it('omits text when not provided', () => { + const result = formatAccessibilityValue({ max: 60, now: 30 }); + expect(result).not.toHaveProperty('text'); + }); +}); + +describe('mergeAccessibilityActions', () => { + it('merges and deduplicates by name (later wins)', () => { + const merged = mergeAccessibilityActions( + [{ name: 'activate', label: 'old' }, { name: 'react' }], + [{ name: 'activate', label: 'new' }, { name: 'reply' }], + ); + expect(merged).toEqual([ + { name: 'activate', label: 'new' }, + { name: 'react' }, + { name: 'reply' }, + ]); + }); + + it('skips undefined inputs', () => { + expect(mergeAccessibilityActions(undefined, [{ name: 'a' }])).toEqual([{ name: 'a' }]); + }); + + it('returns empty array when no inputs', () => { + expect(mergeAccessibilityActions()).toEqual([]); + }); +}); diff --git a/package/src/a11y/a11yUtils.ts b/package/src/a11y/a11yUtils.ts new file mode 100644 index 0000000000..b026d47fde --- /dev/null +++ b/package/src/a11y/a11yUtils.ts @@ -0,0 +1,50 @@ +/** + * Compose a single accessibility label from multiple parts. + * Empty/null/undefined parts are filtered out, the remainder joined with a comma+space + * so screen readers add a brief pause between segments. + */ +export const composeAccessibilityLabel = ( + ...parts: Array +): string => parts.filter((p): p is string => typeof p === 'string' && p.length > 0).join(', '); + +/** + * Build the value object passed to `accessibilityValue` for progress/seek surfaces. + * Mirrors the React SDK's `progressBarA11y.ts` shape. + */ +export const formatAccessibilityValue = ({ + max, + min = 0, + now, + text, +}: { + max: number; + now: number; + min?: number; + text?: string; +}): { max: number; min: number; now: number; text?: string } => { + const value: { max: number; min: number; now: number; text?: string } = { + max, + min, + now: Math.min(Math.max(min, now), max), + }; + if (text) value.text = text; + return value; +}; + +/** + * Merge two `accessibilityActions` arrays, deduplicating by `name` (later wins). + */ +type A11yAction = { name: string; label?: string }; + +export const mergeAccessibilityActions = ( + ...actionLists: Array +): A11yAction[] => { + const byName = new Map(); + for (const list of actionLists) { + if (!list) continue; + for (const action of list) { + byName.set(action.name, action); + } + } + return Array.from(byName.values()); +}; diff --git a/package/src/a11y/hooks/useA11yLabel.ts b/package/src/a11y/hooks/useA11yLabel.ts new file mode 100644 index 0000000000..7f2260d4c1 --- /dev/null +++ b/package/src/a11y/hooks/useA11yLabel.ts @@ -0,0 +1,19 @@ +import { useAccessibilityContext } from '../../contexts/accessibilityContext/AccessibilityContext'; +import { useTranslationContext } from '../../contexts/translationContext/TranslationContext'; + +/** + * Returns the translated `aria/...` label when the AccessibilityContext is enabled, + * or `undefined` when disabled. Components pass the result straight to + * `accessibilityLabel` so the i18n lookup is skipped on hot list paths in the + * default disabled-state. + * + * Example: + * const label = useA11yLabel('aria/Avatar of {{name}}', { name }); + * + */ +export const useA11yLabel = (key: string, params?: Record): string | undefined => { + const { enabled } = useAccessibilityContext(); + const { t } = useTranslationContext(); + if (!enabled) return undefined; + return t(key, params); +}; diff --git a/package/src/a11y/hooks/useAnnounceOnStateChange.ts b/package/src/a11y/hooks/useAnnounceOnStateChange.ts new file mode 100644 index 0000000000..5807c6dcaf --- /dev/null +++ b/package/src/a11y/hooks/useAnnounceOnStateChange.ts @@ -0,0 +1,40 @@ +import { useEffect, useRef } from 'react'; + +import { useAccessibilityAnnouncer } from '../../components/Accessibility/useAccessibilityAnnouncer'; + +const DEFAULT_DEBOUNCE_MS = 250; + +/** + * Announces `message` whenever it changes (and is non-empty), with a debounce + * to avoid spamming the screen reader on rapid transitions. + * + * Used by `AITypingIndicatorView` ("Thinking…" → "Generating…" → idle) and + * the `Indicators` family (loading → loaded → error). + */ +export const useAnnounceOnStateChange = ( + message: string | null | undefined, + options: { debounceMs?: number; priority?: 'polite' | 'assertive' } = {}, +) => { + const { debounceMs = DEFAULT_DEBOUNCE_MS, priority = 'polite' } = options; + const announce = useAccessibilityAnnouncer(); + const timeoutRef = useRef | null>(null); + const lastAnnouncedRef = useRef(null); + + useEffect(() => { + if (!message || message === lastAnnouncedRef.current) return; + + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => { + announce(message, priority); + lastAnnouncedRef.current = message; + timeoutRef.current = null; + }, debounceMs); + + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + }, [announce, debounceMs, message, priority]); +}; diff --git a/package/src/a11y/hooks/useReducedMotionPreference.ts b/package/src/a11y/hooks/useReducedMotionPreference.ts new file mode 100644 index 0000000000..8096d03f4b --- /dev/null +++ b/package/src/a11y/hooks/useReducedMotionPreference.ts @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react'; +import { AccessibilityInfo } from 'react-native'; + +import { useAccessibilityContext } from '../../contexts/accessibilityContext/AccessibilityContext'; + +/** + * Subscribes to AccessibilityInfo reduce-motion changes and returns the live state. + * Returns false when the AccessibilityContext is disabled. + */ +export const useReducedMotionPreference = (): boolean => { + const { enabled } = useAccessibilityContext(); + const [reduceMotion, setReduceMotion] = useState(false); + + useEffect(() => { + if (!enabled) { + setReduceMotion(false); + return; + } + + let cancelled = false; + AccessibilityInfo.isReduceMotionEnabled?.() + .then((value) => { + if (!cancelled) setReduceMotion(value); + }) + .catch(() => { + // Older RN or platforms without the API. + }); + + const subscription = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduceMotion); + + return () => { + cancelled = true; + subscription?.remove?.(); + }; + }, [enabled]); + + return enabled && reduceMotion; +}; diff --git a/package/src/a11y/hooks/useResolvedModalAccessibilityProps.ts b/package/src/a11y/hooks/useResolvedModalAccessibilityProps.ts new file mode 100644 index 0000000000..b243cbe2e7 --- /dev/null +++ b/package/src/a11y/hooks/useResolvedModalAccessibilityProps.ts @@ -0,0 +1,30 @@ +import { Platform } from 'react-native'; + +import { useAccessibilityContext } from '../../contexts/accessibilityContext/AccessibilityContext'; + +export type ResolvedModalAccessibilityProps = { + accessibilityViewIsModal?: boolean; + importantForAccessibility?: 'auto' | 'yes' | 'no' | 'no-hide-descendants'; + accessibilityRole?: 'none' | 'button' | 'image' | 'text' | 'alert' | 'menu' | 'menuitem'; +}; + +/** + * Returns the platform-appropriate set of a11y props for a modal/sheet root. + * Equivalent of stream-chat-react's `useResolvedModalAriaProps` — but aware of + * RN's iOS-vs-Android split: + * - iOS uses `accessibilityViewIsModal` to trap focus. + * - Android uses `importantForAccessibility="yes"` on the modal root and + * `"no-hide-descendants"` on background siblings (caller's responsibility). + * + * Returns an empty object when AccessibilityContext is disabled, so the modal + * stays a no-op for integrators that haven't opted in. + */ +export const useResolvedModalAccessibilityProps = (): ResolvedModalAccessibilityProps => { + const { enabled } = useAccessibilityContext(); + if (!enabled) return {}; + + if (Platform.OS === 'ios') { + return { accessibilityViewIsModal: true }; + } + return { importantForAccessibility: 'yes' }; +}; diff --git a/package/src/a11y/hooks/useScreenReaderEnabled.ts b/package/src/a11y/hooks/useScreenReaderEnabled.ts new file mode 100644 index 0000000000..c2f37f4aae --- /dev/null +++ b/package/src/a11y/hooks/useScreenReaderEnabled.ts @@ -0,0 +1,44 @@ +import { useEffect, useState } from 'react'; +import { AccessibilityInfo } from 'react-native'; + +import { useAccessibilityContext } from '../../contexts/accessibilityContext/AccessibilityContext'; + +/** + * Subscribes to AccessibilityInfo screen-reader changes and returns the live state. + * Returns false when the AccessibilityContext is disabled, regardless of the OS state, + * so consumers don't pay the listener cost when the SDK's a11y is opted out. + * + * `forceScreenReaderMode: true` in the config short-circuits to true (used in tests + * and for integrator preview). + */ +export const useScreenReaderEnabled = (): boolean => { + const { enabled, forceScreenReaderMode } = useAccessibilityContext(); + const [isEnabled, setIsEnabled] = useState(false); + + useEffect(() => { + if (!enabled) { + setIsEnabled(false); + return; + } + + let cancelled = false; + AccessibilityInfo.isScreenReaderEnabled() + .then((value) => { + if (!cancelled) setIsEnabled(value); + }) + .catch(() => { + // Some platforms / environments may not implement this; fall back to false. + }); + + const subscription = AccessibilityInfo.addEventListener('screenReaderChanged', setIsEnabled); + + return () => { + cancelled = true; + subscription?.remove?.(); + }; + }, [enabled]); + + if (!enabled) return false; + if (forceScreenReaderMode) return true; + return isEnabled; +}; diff --git a/package/src/a11y/index.ts b/package/src/a11y/index.ts new file mode 100644 index 0000000000..55ae3fc9ac --- /dev/null +++ b/package/src/a11y/index.ts @@ -0,0 +1,6 @@ +export * from './a11yUtils'; +export * from './hooks/useScreenReaderEnabled'; +export * from './hooks/useReducedMotionPreference'; +export * from './hooks/useResolvedModalAccessibilityProps'; +export * from './hooks/useAnnounceOnStateChange'; +export * from './hooks/useA11yLabel'; diff --git a/package/src/components/Accessibility/AccessibilityAnnouncer.tsx b/package/src/components/Accessibility/AccessibilityAnnouncer.tsx new file mode 100644 index 0000000000..3d09f4bddc --- /dev/null +++ b/package/src/components/Accessibility/AccessibilityAnnouncer.tsx @@ -0,0 +1,78 @@ +import React, { PropsWithChildren, useCallback, useEffect, useMemo, useRef } from 'react'; +import { AccessibilityInfo } from 'react-native'; + +import { + AccessibilityAnnounce, + AccessibilityAnnouncerContext, + AccessibilityAnnouncePriority, +} from './useAccessibilityAnnouncer'; + +import { useAccessibilityContext } from '../../contexts/accessibilityContext/AccessibilityContext'; + +const ANNOUNCE_DEBOUNCE_MS = 50; + +type SequenceByPriority = { [key in AccessibilityAnnouncePriority]: number }; +type TimeoutByPriority = { + [key in AccessibilityAnnouncePriority]: ReturnType | undefined; +}; + +/** + * Provider that exposes an imperative announcer mirroring stream-chat-react's + * `AriaLiveRegion`. On RN, both iOS and Android use the imperative + * `AccessibilityInfo.announceForAccessibility` (RN's cross-platform announcer); + * the sequence/debounce pattern is preserved so repeat announcements with the + * same text still fire (otherwise screen readers may swallow them). + * + * No-op when AccessibilityContext.enabled is false — children are returned + * untouched and no listeners or context are attached. + */ +export const AccessibilityAnnouncer = ({ children }: PropsWithChildren) => { + const { enabled } = useAccessibilityContext(); + const sequenceByPriorityRef = useRef({ assertive: 0, polite: 0 }); + const timeoutByPriorityRef = useRef({ + assertive: undefined, + polite: undefined, + }); + const unmountedRef = useRef(false); + + const clearPendingTimeout = useCallback((priority: AccessibilityAnnouncePriority) => { + if (!timeoutByPriorityRef.current[priority]) return; + clearTimeout(timeoutByPriorityRef.current[priority]); + timeoutByPriorityRef.current[priority] = undefined; + }, []); + + const announce = useCallback( + (message, priority = 'polite') => { + if (!message) return; + const sequence = sequenceByPriorityRef.current[priority] + 1; + sequenceByPriorityRef.current[priority] = sequence; + clearPendingTimeout(priority); + timeoutByPriorityRef.current[priority] = setTimeout(() => { + if (unmountedRef.current) return; + if (sequenceByPriorityRef.current[priority] !== sequence) return; + AccessibilityInfo.announceForAccessibility(message); + timeoutByPriorityRef.current[priority] = undefined; + }, ANNOUNCE_DEBOUNCE_MS); + }, + [clearPendingTimeout], + ); + + useEffect(() => { + unmountedRef.current = false; + return () => { + unmountedRef.current = true; + clearPendingTimeout('assertive'); + clearPendingTimeout('polite'); + }; + }, [clearPendingTimeout]); + + const contextValue = useMemo(() => ({ announce }), [announce]); + + if (!enabled) return <>{children}; + + return ( + + {children} + + ); +}; diff --git a/package/src/components/Accessibility/NotificationAnnouncer.tsx b/package/src/components/Accessibility/NotificationAnnouncer.tsx new file mode 100644 index 0000000000..d149be866c --- /dev/null +++ b/package/src/components/Accessibility/NotificationAnnouncer.tsx @@ -0,0 +1,43 @@ +import { useEffect, useRef } from 'react'; + +import { useAccessibilityAnnouncer } from './useAccessibilityAnnouncer'; + +import { useAccessibilityContext } from '../../contexts/accessibilityContext/AccessibilityContext'; +import { useChatContext } from '../../contexts/chatContext/ChatContext'; +import { useTranslationContext } from '../../contexts/translationContext/TranslationContext'; + +/** + * Mirrors stream-chat-react's ``. RN does not yet have a + * unified Notification queue, so this component currently announces only + * connection-state transitions (offline → online and back) gated on + * `accessibility.announceConnectionState`. Per-channel error announcements can + * be wired in by a future PR via `useChannelContext().error`. + * + * Renders nothing. Mount once inside `` (or wherever the active chat + * surface lives). + */ +export const NotificationAnnouncer = () => { + const { announceConnectionState, enabled } = useAccessibilityContext(); + const { connectionRecovering, isOnline } = useChatContext(); + const announce = useAccessibilityAnnouncer(); + const { t } = useTranslationContext(); + const previousIsOnlineRef = useRef(undefined); + + useEffect(() => { + if (!enabled || !announceConnectionState) return; + if (previousIsOnlineRef.current === undefined) { + previousIsOnlineRef.current = isOnline; + return; + } + if (previousIsOnlineRef.current === isOnline) return; + previousIsOnlineRef.current = isOnline; + + if (isOnline) { + announce(t('aria/Connected'), 'polite'); + } else { + announce(connectionRecovering ? t('aria/Reconnecting') : t('aria/Offline'), 'assertive'); + } + }, [announce, announceConnectionState, connectionRecovering, enabled, isOnline, t]); + + return null; +}; diff --git a/package/src/components/Accessibility/__tests__/AccessibilityAnnouncer.test.tsx b/package/src/components/Accessibility/__tests__/AccessibilityAnnouncer.test.tsx new file mode 100644 index 0000000000..bb68d39ced --- /dev/null +++ b/package/src/components/Accessibility/__tests__/AccessibilityAnnouncer.test.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { AccessibilityInfo } from 'react-native'; + +import { renderHook, waitFor } from '@testing-library/react-native'; + +import { AccessibilityAnnouncer } from '../AccessibilityAnnouncer'; +import { useAccessibilityAnnouncer } from '../useAccessibilityAnnouncer'; + +import { AccessibilityProvider } from '../../../contexts/accessibilityContext/AccessibilityContext'; + +jest.mock('react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo', () => ({ + __esModule: true, + default: { + announceForAccessibility: jest.fn(), + addEventListener: jest.fn().mockReturnValue({ remove: jest.fn() }), + isScreenReaderEnabled: jest.fn().mockResolvedValue(false), + isReduceMotionEnabled: jest.fn().mockResolvedValue(false), + }, +})); + +const wrapper = + (enabled: boolean) => + ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + +describe('AccessibilityAnnouncer', () => { + beforeEach(() => { + (AccessibilityInfo.announceForAccessibility as jest.Mock).mockClear(); + }); + + it('returns a no-op when enabled is false', async () => { + const { result } = renderHook(() => useAccessibilityAnnouncer(), { wrapper: wrapper(false) }); + result.current('hello'); + await new Promise((r) => setTimeout(r, 80)); + expect(AccessibilityInfo.announceForAccessibility).not.toHaveBeenCalled(); + }); + + it('announces via AccessibilityInfo when enabled', async () => { + const { result } = renderHook(() => useAccessibilityAnnouncer(), { wrapper: wrapper(true) }); + result.current('hello'); + await waitFor(() => + expect(AccessibilityInfo.announceForAccessibility).toHaveBeenCalledWith('hello'), + ); + }); + + it('flushes only the latest message per priority within the debounce window', async () => { + const { result } = renderHook(() => useAccessibilityAnnouncer(), { wrapper: wrapper(true) }); + result.current('first'); + result.current('second'); + result.current('third'); + await waitFor(() => + expect(AccessibilityInfo.announceForAccessibility).toHaveBeenCalledWith('third'), + ); + expect(AccessibilityInfo.announceForAccessibility).not.toHaveBeenCalledWith('first'); + expect(AccessibilityInfo.announceForAccessibility).not.toHaveBeenCalledWith('second'); + }); + + it('ignores empty messages', async () => { + const { result } = renderHook(() => useAccessibilityAnnouncer(), { wrapper: wrapper(true) }); + result.current(''); + await new Promise((r) => setTimeout(r, 80)); + expect(AccessibilityInfo.announceForAccessibility).not.toHaveBeenCalled(); + }); +}); diff --git a/package/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts b/package/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts new file mode 100644 index 0000000000..9f7dd2b81f --- /dev/null +++ b/package/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts @@ -0,0 +1,157 @@ +import { useCallback, useEffect, useRef } from 'react'; + +import type { Channel, Event, MessageResponse } from 'stream-chat'; + +import { useAccessibilityContext } from '../../../contexts/accessibilityContext/AccessibilityContext'; +import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext'; +import { useAccessibilityAnnouncer } from '../useAccessibilityAnnouncer'; + +const MESSAGE_ANNOUNCEMENT_THROTTLE_MS = 1000; + +const isAnnounceableIncomingMessage = (message: MessageResponse, ownUserId?: string): boolean => { + const messageUserId = message.user?.id; + if (!message.id || !messageUserId || messageUserId === ownUserId) return false; + return ( + message.type !== 'deleted' && + message.type !== 'ephemeral' && + message.type !== 'error' && + message.type !== 'system' && + message.status !== 'failed' && + message.status !== 'sending' + ); +}; + +const getSenderName = ( + message: MessageResponse, + t: ReturnType['t'], +) => message.user?.name?.trim() || message.user?.id || t('Anonymous'); + +export type UseIncomingMessageAnnouncementsParams = { + activeThreadId?: string; + channel?: Channel; + ownUserId?: string; + threadList?: boolean; +}; + +/** + * Mirrors stream-chat-react's `useIncomingMessageAnnouncements`: + * - 1 message → "New message from {{user}}" + * - >1 messages within throttle window → "{{count}} new messages" + * - Throttled to one announcement per second + * - Bounded `announcedMessageIds` set so a long-running session does not leak. + * + * Subscribes to `channel.on('message.new')`. When AccessibilityContext.enabled + * is false OR `announceNewMessages` is false, the hook is a no-op (no + * subscription is opened, no listener cost is paid). + */ +export const useIncomingMessageAnnouncements = ({ + activeThreadId, + channel, + ownUserId, + threadList = false, +}: UseIncomingMessageAnnouncementsParams) => { + const { announceNewMessages, enabled } = useAccessibilityContext(); + const announce = useAccessibilityAnnouncer(); + const { t } = useTranslationContext(); + const lastAnnouncementTimestampRef = useRef(0); + const flushTimeoutRef = useRef | undefined>(undefined); + const announcedMessageIdsRef = useRef(new Set()); + const pendingAnnouncementBatchRef = useRef<{ count: number; firstSender: string | null }>({ + count: 0, + firstSender: null, + }); + + const flushPendingAnnouncements = useCallback(() => { + const pending = pendingAnnouncementBatchRef.current; + if (pending.count <= 0) return; + + if (pending.count === 1) { + announce( + t('aria/New message from {{user}}', { + user: pending.firstSender || t('Anonymous'), + }), + ); + } else { + announce(t('aria/{{count}} new messages', { count: pending.count })); + } + + pending.count = 0; + pending.firstSender = null; + lastAnnouncementTimestampRef.current = Date.now(); + }, [announce, t]); + + const scheduleFlush = useCallback(() => { + if (flushTimeoutRef.current) return; + const now = Date.now(); + const elapsed = now - lastAnnouncementTimestampRef.current; + if (elapsed >= MESSAGE_ANNOUNCEMENT_THROTTLE_MS) { + flushPendingAnnouncements(); + return; + } + flushTimeoutRef.current = setTimeout(() => { + flushTimeoutRef.current = undefined; + flushPendingAnnouncements(); + }, MESSAGE_ANNOUNCEMENT_THROTTLE_MS - elapsed); + }, [flushPendingAnnouncements]); + + useEffect( + () => () => { + if (flushTimeoutRef.current) clearTimeout(flushTimeoutRef.current); + }, + [], + ); + + useEffect(() => { + if (!enabled || !announceNewMessages || !channel) return; + + const handleMessageNew = (event: Event) => { + const message = event.message; + if (!message) return; + if ( + (event.cid && event.cid !== channel.cid) || + !isAnnounceableIncomingMessage(message, ownUserId) + ) { + return; + } + + const isReply = !!message.parent_id; + const belongsToActiveThread = !!activeThreadId && message.parent_id === activeThreadId; + const shouldAnnounceInThreadList = threadList && belongsToActiveThread; + const shouldAnnounceInMainList = !threadList && !isReply; + if (!shouldAnnounceInThreadList && !shouldAnnounceInMainList) return; + + if (announcedMessageIdsRef.current.has(message.id || '')) return; + if (message.id) { + if (announcedMessageIdsRef.current.size > 500) { + announcedMessageIdsRef.current.clear(); + } + announcedMessageIdsRef.current.add(message.id); + } + + pendingAnnouncementBatchRef.current.count += 1; + if (!pendingAnnouncementBatchRef.current.firstSender) { + pendingAnnouncementBatchRef.current.firstSender = getSenderName(message, t); + } + + scheduleFlush(); + }; + + const subscription = channel.on('message.new', handleMessageNew); + return () => { + subscription.unsubscribe(); + if (flushTimeoutRef.current) { + clearTimeout(flushTimeoutRef.current); + flushTimeoutRef.current = undefined; + } + }; + }, [ + activeThreadId, + announceNewMessages, + channel, + enabled, + ownUserId, + scheduleFlush, + t, + threadList, + ]); +}; diff --git a/package/src/components/Accessibility/index.ts b/package/src/components/Accessibility/index.ts new file mode 100644 index 0000000000..e71f13cf4f --- /dev/null +++ b/package/src/components/Accessibility/index.ts @@ -0,0 +1,4 @@ +export * from './AccessibilityAnnouncer'; +export * from './NotificationAnnouncer'; +export * from './useAccessibilityAnnouncer'; +export * from './hooks/useIncomingMessageAnnouncements'; diff --git a/package/src/components/Accessibility/useAccessibilityAnnouncer.ts b/package/src/components/Accessibility/useAccessibilityAnnouncer.ts new file mode 100644 index 0000000000..39cdc4a16d --- /dev/null +++ b/package/src/components/Accessibility/useAccessibilityAnnouncer.ts @@ -0,0 +1,30 @@ +import { createContext, useContext } from 'react'; + +export type AccessibilityAnnouncePriority = 'assertive' | 'polite'; +export type AccessibilityAnnounce = ( + message: string, + priority?: AccessibilityAnnouncePriority, +) => void; + +export type AccessibilityAnnouncerContextValue = { + announce: AccessibilityAnnounce; +}; + +const noopAnnounce: AccessibilityAnnounce = () => undefined; + +export const AccessibilityAnnouncerContext = createContext< + AccessibilityAnnouncerContextValue | undefined +>(undefined); + +/** + * Returns the imperative announcer. When called outside the AccessibilityAnnouncer + * provider (which happens any time the SDK's a11y is disabled, since the provider + * doesn't mount in that case), this returns a no-op. + * + * Mirrors the React SDK's `useAriaLiveAnnouncer` so cross-SDK code reads the same. + */ +export const useAccessibilityAnnouncer = (): AccessibilityAnnounce => { + const contextValue = useContext(AccessibilityAnnouncerContext); + if (!contextValue) return noopAnnounce; + return contextValue.announce; +}; diff --git a/package/src/components/Chat/Chat.tsx b/package/src/components/Chat/Chat.tsx index 89df8a737e..d623dae232 100644 --- a/package/src/components/Chat/Chat.tsx +++ b/package/src/components/Chat/Chat.tsx @@ -8,6 +8,11 @@ import { useAppSettings } from './hooks/useAppSettings'; import { useCreateChatContext } from './hooks/useCreateChatContext'; import { useIsOnline } from './hooks/useIsOnline'; +import { AccessibilityAnnouncer } from '../../components/Accessibility/AccessibilityAnnouncer'; +import { + AccessibilityConfig, + AccessibilityProvider, +} from '../../contexts/accessibilityContext/AccessibilityContext'; import { ChannelsStateProvider } from '../../contexts/channelsStateContext/ChannelsStateContext'; import { ChatContextValue, ChatProvider } from '../../contexts/chatContext/ChatContext'; import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; @@ -135,6 +140,14 @@ export type ChatProps = Pick & * @overrideType object */ style?: DeepPartial; + /** + * Opt-in accessibility configuration. A11y is OFF by default — pass + * `accessibility={{ enabled: true }}` to enable screen-reader announcements, + * gesture alternatives, and semantic a11y attributes across the SDK. + * + * See `AccessibilityConfig` for the full set of toggles. + */ + accessibility?: AccessibilityConfig; }; const selector = (nextValue: OfflineDBState) => @@ -145,6 +158,7 @@ const selector = (nextValue: OfflineDBState) => const ChatWithContext = (props: PropsWithChildren) => { const { + accessibility, children, client, closeConnectionOnBackground = true, @@ -286,7 +300,11 @@ const ChatWithContext = (props: PropsWithChildren) => { - {children} + + + {children} + + diff --git a/package/src/components/index.ts b/package/src/components/index.ts index 9a22b3dde0..ea33fbb759 100644 --- a/package/src/components/index.ts +++ b/package/src/components/index.ts @@ -1,3 +1,5 @@ +export * from './Accessibility'; + export * from './Attachment/Attachment'; export * from './Attachment/Audio'; export * from './Attachment/UrlPreview'; diff --git a/package/src/contexts/accessibilityContext/AccessibilityContext.tsx b/package/src/contexts/accessibilityContext/AccessibilityContext.tsx new file mode 100644 index 0000000000..a293d1d899 --- /dev/null +++ b/package/src/contexts/accessibilityContext/AccessibilityContext.tsx @@ -0,0 +1,59 @@ +import React, { PropsWithChildren, useContext, useMemo } from 'react'; + +/** Tri-state for gesture-alternative toggles. */ +export type A11yMode = 'auto' | 'always' | 'never'; + +export type AccessibilityConfig = { + /** + * Master toggle. Default FALSE — integrators must opt in. When false, the SDK + * behaves exactly as it does today; no a11y attributes are added, no announcer + * mounts, no listeners attached. + */ + enabled?: boolean; + /** For testing — force "screen reader on" UI even when no SR is active. */ + forceScreenReaderMode?: boolean; + /** Announce new messages via the announcer. Default true (when enabled). */ + announceNewMessages?: boolean; + /** Announce typing indicator. Default false (noisy on mobile). */ + announceTypingIndicator?: boolean; + /** Announce connection state (offline/online). Default true. */ + announceConnectionState?: boolean; + /** Audio recorder gesture-alternative. 'auto' (default), 'always', 'never'. */ + audioRecorderTapMode?: A11yMode; + /** Image gallery gesture-alternative. 'auto' (default), 'always', 'never'. */ + imageGalleryScreenReaderMode?: A11yMode; + /** Message actions trigger. 'long-press' (no alt button), 'auto' (default — show button when SR is on), 'always-button'. */ + messageActionsTrigger?: 'long-press' | 'auto' | 'always-button'; +}; + +/** Fully-resolved config — every field is populated with its default. */ +export type ResolvedAccessibilityConfig = Required; + +export const accessibilityContextDefaultValue: ResolvedAccessibilityConfig = { + announceConnectionState: true, + announceNewMessages: true, + announceTypingIndicator: false, + audioRecorderTapMode: 'auto', + enabled: false, + forceScreenReaderMode: false, + imageGalleryScreenReaderMode: 'auto', + messageActionsTrigger: 'auto', +}; + +export const AccessibilityContext = React.createContext( + accessibilityContextDefaultValue, +); + +export const AccessibilityProvider = ({ + children, + value, +}: PropsWithChildren<{ value?: AccessibilityConfig }>) => { + const resolved = useMemo( + () => ({ ...accessibilityContextDefaultValue, ...value }), + [value], + ); + + return {children}; +}; + +export const useAccessibilityContext = () => useContext(AccessibilityContext); diff --git a/package/src/contexts/accessibilityContext/__tests__/AccessibilityContext.test.tsx b/package/src/contexts/accessibilityContext/__tests__/AccessibilityContext.test.tsx new file mode 100644 index 0000000000..96ad075a61 --- /dev/null +++ b/package/src/contexts/accessibilityContext/__tests__/AccessibilityContext.test.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import { renderHook } from '@testing-library/react-native'; + +import { + AccessibilityProvider, + accessibilityContextDefaultValue, + useAccessibilityContext, +} from '../AccessibilityContext'; + +const wrap = + (props: Parameters[0]) => + ({ children }: { children: React.ReactNode }) => ( + {children} + ); + +describe('AccessibilityContext', () => { + it('defaults to disabled', () => { + const { result } = renderHook(() => useAccessibilityContext(), { + wrapper: wrap({ children: null }), + }); + expect(result.current.enabled).toBe(false); + expect(result.current).toEqual(accessibilityContextDefaultValue); + }); + + it('merges integrator config with defaults', () => { + const { result } = renderHook(() => useAccessibilityContext(), { + wrapper: wrap({ children: null, value: { enabled: true, audioRecorderTapMode: 'always' } }), + }); + expect(result.current.enabled).toBe(true); + expect(result.current.audioRecorderTapMode).toBe('always'); + expect(result.current.announceNewMessages).toBe(true); + expect(result.current.announceTypingIndicator).toBe(false); + }); + + it('returns defaults when used outside the provider', () => { + const { result } = renderHook(() => useAccessibilityContext()); + expect(result.current).toEqual(accessibilityContextDefaultValue); + }); +}); diff --git a/package/src/contexts/accessibilityContext/index.ts b/package/src/contexts/accessibilityContext/index.ts new file mode 100644 index 0000000000..846c6747d8 --- /dev/null +++ b/package/src/contexts/accessibilityContext/index.ts @@ -0,0 +1 @@ +export * from './AccessibilityContext'; diff --git a/package/src/contexts/index.ts b/package/src/contexts/index.ts index 7c50dd7997..09046c5685 100644 --- a/package/src/contexts/index.ts +++ b/package/src/contexts/index.ts @@ -1,3 +1,4 @@ +export * from './accessibilityContext/AccessibilityContext'; export * from './attachmentPickerContext/AttachmentPickerContext'; export * from './componentsContext/ComponentsContext'; export * from './bottomSheetContext/BottomSheetContext'; diff --git a/package/src/hooks/index.ts b/package/src/hooks/index.ts index cb5e0f9516..91c9dde3eb 100644 --- a/package/src/hooks/index.ts +++ b/package/src/hooks/index.ts @@ -1,3 +1,4 @@ +export * from '../a11y'; export * from './useAppStateListener'; export * from './useStreami18n'; export * from './useViewport'; diff --git a/package/src/i18n/en.json b/package/src/i18n/en.json index 26fb6a4fae..760f0bebeb 100644 --- a/package/src/i18n/en.json +++ b/package/src/i18n/en.json @@ -1,4 +1,7 @@ { + "+{{count}} More Options_many": "+{{count}} More Options", + "+{{count}} More Options_one": "+{{count}} More Option", + "+{{count}} More Options_other": "+{{count}} More Options", "+{{count}}_many": "+{{count}}", "+{{count}}_one": "+{{count}}", "+{{count}}_other": "+{{count}}", @@ -11,8 +14,13 @@ "Allow access to your Gallery": "Allow access to your Gallery", "Allow camera access in device settings": "Allow camera access in device settings", "Also send to channel": "Also send to channel", + "Also sent in channel": "Also sent in channel", "Anonymous": "Anonymous", "Anonymous voting": "Anonymous voting", + "Archive Chat": "Archive Chat", + "Archive Group": "Archive Group", + "Are you sure you want to delete this chat? This can't be undone.": "Are you sure you want to delete this chat? This can't be undone.", + "Are you sure you want to delete this group? This can't be undone.": "Are you sure you want to delete this group? This can't be undone.", "Are you sure you want to permanently delete this message?": "Are you sure you want to permanently delete this message?", "Are you sure?": "Are you sure?", "Ask a question": "Ask a question", @@ -21,23 +29,32 @@ "Block User": "Block User", "Cancel": "Cancel", "Cannot Flag Message": "Cannot Flag Message", + "Change in Settings": "Change in Settings", + "Choose between 2–10 options": "Choose between 2–10 options", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Consider how your comment might make others feel and be sure to follow our Community Guidelines", "Copy Message": "Copy Message", + "Couldn't load new threads. Tap to retry": "Couldn't load new threads. Tap to retry", "Create Poll": "Create Poll", + "Create a poll and let everyone vote": "Create a poll and let everyone vote", "Delete": "Delete", + "Delete Chat": "Delete Chat", + "Delete Group": "Delete Group", "Delete Message": "Delete Message", + "Delete chat": "Delete chat", "Delete for me": "Delete for me", + "Delete group": "Delete group", "Device camera is used to take photos or videos.": "Device camera is used to take photos or videos.", "Device gallery permissions is used to take photos or videos.": "Device gallery permissions is used to take photos or videos.", "Do you want to send a copy of this message to a moderator for further investigation?": "Do you want to send a copy of this message to a moderator for further investigation?", + "Draft": "Draft", "Due since {{ dueSince }}": "Due since {{ dueSince }}", "Edit Message": "Edit Message", "Edited": "Edited", "Editing Message": "Editing Message", "Emoji matching": "Emoji matching", "Empty message...": "Empty message...", - "Enter a new option": "Enter a new option", "End Vote": "End Vote", + "Enter a new option": "Enter a new option", "Error loading": "Error loading", "Error loading channel list...": "Error loading channel list...", "Error loading messages for this channel...": "Error loading messages for this channel...", @@ -55,8 +72,11 @@ "Hold to start recording.": "Hold to start recording.", "How about sending your first message to a friend?": "How about sending your first message to a friend?", "Instant Commands": "Instant Commands", + "Leave Chat": "Leave Chat", + "Leave Group": "Leave Group", "Let others add options": "Let others add options", "Let's start chatting!": "Let's start chatting!", + "Limit votes per person": "Limit votes per person", "Links are disabled": "Links are disabled", "Live Location": "Live Location", "Loading channels...": "Loading channels...", @@ -68,24 +88,29 @@ "Maximum number of files reached": "Maximum number of files reached", "Message Reactions": "Message Reactions", "Message deleted": "Message deleted", + "Message failed to send": "Message failed to send", "Message flagged": "Message flagged", "Multiple votes": "Multiple votes", - "Network error": "Network error", - "Select more than one option": "Select more than one option", - "Limit votes per person": "Limit votes per person", - "Choose between 2–10 options": "Choose between 2–10 options", + "Mute Group": "Mute Group", "Mute User": "Mute User", + "Network error": "Network error", "No chats here yet…": "No chats here yet…", + "No conversations yet": "No conversations yet", "No items exist": "No items exist", + "No messages yet": "No messages yet", "No threads here yet": "No threads here yet", "Not supported": "Not supported", "Nothing yet...": "Nothing yet...", + "Offline": "Offline", "Ok": "Ok", + "Online": "Online", "Only visible to you": "Only visible to you", + "Open Camera": "Open Camera", + "Open Files": "Open Files", "Open Settings": "Open Settings", "Option": "Option", - "Option {{count}}": "Option {{count}}", "Option already exists": "Option already exists", + "Option {{count}}": "Option {{count}}", "Options": "Options", "Photo": "Photo", "Photos and Videos": "Photos and Videos", @@ -97,16 +122,23 @@ "Poll Comments": "Poll Comments", "Poll Options": "Poll Options", "Poll Results": "Poll Results", + "Poll has ended": "Poll has ended", "Questions": "Questions", "Reconnecting...": "Reconnecting...", + "Reminder overdue": "Reminder overdue", + "Reminder set": "Reminder set", + "Replied to a thread": "Replied to a thread", "Reply": "Reply", - "Reply to {{name}}": "Reply to {{name}}", "Reply to Message": "Reply to Message", + "Reply to a message to start a thread": "Reply to a message to start a thread", + "Reply to {{name}}": "Reply to {{name}}", "Resend": "Resend", "Retry Upload": "Retry Upload", "SEND": "SEND", "Search": "Search", "Select More Photos": "Select More Photos", + "Select files to share": "Select files to share", + "Select more than one option": "Select more than one option", "Select one": "Select one", "Select one or more": "Select one or more", "Select up to {{count}}_many": "Select up to {{count}}", @@ -120,29 +152,61 @@ "Slow mode ON": "Slow mode ON", "Slow mode, wait {{seconds}}s...": "Slow mode, wait {{seconds}}s...", "Suggest an option": "Suggest an option", + "Take a photo and share": "Take a photo and share", + "Take a video and share": "Take a video and share", + "Tap to remove": "Tap to remove", "The message has been reported to a moderator.": "The message has been reported to a moderator.", "The source message was deleted": "The source message was deleted", "Thinking...": "Thinking...", "This reply was deleted": "This reply was deleted", "Thread Reply": "Thread Reply", "Type a number from 2 to 10": "Type a number from 2 to 10", + "Typing": "Typing", + "Unarchive Chat": "Unarchive Chat", + "Unarchive Group": "Unarchive Group", "Unban User": "Unban User", "Unblock User": "Unblock User", "Unknown User": "Unknown User", + "Unmute Group": "Unmute Group", "Unmute User": "Unmute User", "Unpin from Conversation": "Unpin from Conversation", "Unread Messages": "Unread Messages", + "Unsupported Attachment": "Unsupported Attachment", "Update your comment": "Update your comment", "Video": "Video", + "View": "View", "View Results": "View Results", "View {{count}} comments_many": "View {{count}} comments", "View {{count}} comments_one": "View {{count}} comment", "View {{count}} comments_other": "View {{count}} comments", "Voice message": "Voice message", "Voice message ({{duration}})": "Voice message ({{duration}})", - "Your comment": "Your comment", "You": "You", "You can't send messages in this channel": "You can't send messages in this channel", + "You have not granted access to the photo library.": "You have not granted access to the photo library.", + "You have not granted access to your camera": "You have not granted access to your camera", + "You voted: {{ option }}": "You voted: {{ option }}", + "Your comment": "Your comment", + "and {{ count }} others": "and {{ count }} others", + "aria/AI is generating": "AI is generating", + "aria/AI is thinking": "AI is thinking", + "aria/Avatar of {{name}}": "Avatar of {{name}}", + "aria/Connected": "Connected", + "aria/Delivered": "Delivered", + "aria/Loading": "Loading", + "aria/Loading failed": "Loading failed", + "aria/New message from {{user}}": "New message from {{user}}", + "aria/Offline": "Offline", + "aria/Open message actions": "Open message actions", + "aria/Reaction {{emoji}} by {{count}} users": "Reaction {{emoji}} by {{count}} users", + "aria/Read": "Read", + "aria/Reconnecting": "Reconnecting", + "aria/Reply to {{user}}": "Reply to {{user}}", + "aria/Send message": "Send message", + "aria/Sending": "Sending", + "aria/Sent": "Sent", + "aria/Voice message recording. Hold to record.": "Voice message recording. Hold to record.", + "aria/{{count}} new messages": "{{count}} new messages", "duration/Location end at": "{{ milliseconds | durationFormatter(withSuffix: false) }}", "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", @@ -158,16 +222,11 @@ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Yesterday]\", \"lastWeek\":\"dddd\", \"nextDay\":\"[Tomorrow]\", \"nextWeek\":\"dddd [at] LT\", \"sameDay\":\"LT\", \"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} and {{ nonSelfUserLength }} more are typing", + "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} and {{ secondUser }} are typing", "{{ index }} of {{ photoLength }}": "{{ index }} of {{ photoLength }}", + "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} people are typing", "{{ replyCount }} Replies": "{{ replyCount }} Replies", "{{ user }} is typing": "{{ user }} is typing", - "You voted: {{ option }}": "You voted: {{ option }}", - "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} and {{ secondUser }} are typing", - "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} people are typing", - "Typing": "Typing", - "No messages yet": "No messages yet", - "Message failed to send": "Message failed to send", - "and {{ count }} others": "and {{ count }} others", "{{ user }} voted: {{ option }}": "{{ user }} voted: {{ option }}", "{{count}} Audios_many": "{{count}} Audios", "{{count}} Audios_one": "{{count}} Audio", @@ -178,63 +237,23 @@ "{{count}} Photos_many": "{{count}} Photos", "{{count}} Photos_one": "{{count}} Photo", "{{count}} Photos_other": "{{count}} Photos", - "{{count}} Voice messages_many": "{{count}} Voice messages", - "{{count}} Voice messages_one": "{{count}} Voice message", - "{{count}} Voice messages_other": "{{count}} Voice messages", + "{{count}} Reactions_many": "{{count}} Reactions", + "{{count}} Reactions_one": "{{count}} Reaction", + "{{count}} Reactions_other": "{{count}} Reactions", "{{count}} Videos_many": "{{count}} Videos", "{{count}} Videos_one": "{{count}} Video", "{{count}} Videos_other": "{{count}} Videos", + "{{count}} Voice messages_many": "{{count}} Voice messages", + "{{count}} Voice messages_one": "{{count}} Voice message", + "{{count}} Voice messages_other": "{{count}} Voice messages", + "{{count}} new messages": "{{count}} new messages", + "{{count}} new threads": "{{count}} new threads", + "{{count}} unread": "{{count}} unread", "{{count}} votes_many": "{{count}} votes", "{{count}} votes_one": "{{count}} vote", "{{count}} votes_other": "{{count}} votes", - "🏙 Attachment...": "🏙 Attachment...", - "You have not granted access to the photo library.": "You have not granted access to the photo library.", - "Change in Settings": "Change in Settings", - "Create a poll and let everyone vote": "Create a poll and let everyone vote", - "Open Camera": "Open Camera", - "Open Files": "Open Files", - "Select files to share": "Select files to share", - "Take a photo and share": "Take a photo and share", - "Take a video and share": "Take a video and share", - "You have not granted access to your camera": "You have not granted access to your camera", - "{{count}} Reactions_many": "{{count}} Reactions", - "{{count}} Reactions_one": "{{count}} Reaction", - "{{count}} Reactions_other": "{{count}} Reactions", - "Tap to remove": "Tap to remove", - "Draft": "Draft", - "Reminder set": "Reminder set", - "Also sent in channel": "Also sent in channel", - "Replied to a thread": "Replied to a thread", - "View": "View", - "Reminder overdue": "Reminder overdue", - "Poll has ended": "Poll has ended", - "Reply to a message to start a thread": "Reply to a message to start a thread", - "Couldn't load new threads. Tap to retry": "Couldn't load new threads. Tap to retry", - "{{count}} new threads": "{{count}} new threads", - "No conversations yet": "No conversations yet", - "Are you sure you want to delete this group? This can't be undone.": "Are you sure you want to delete this group? This can't be undone.", - "Are you sure you want to delete this chat? This can't be undone.": "Are you sure you want to delete this chat? This can't be undone.", - "Delete chat": "Delete chat", - "Delete group": "Delete group", - "Archive Chat": "Archive Chat", - "Archive Group": "Archive Group", - "Delete Chat": "Delete Chat", - "Delete Group": "Delete Group", - "Leave Chat": "Leave Chat", - "Leave Group": "Leave Group", - "Mute Group": "Mute Group", - "Offline": "Offline", - "Online": "Online", - "Unarchive Chat": "Unarchive Chat", - "Unarchive Group": "Unarchive Group", - "Unmute Group": "Unmute Group", + "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} members, {{onlineCount}} online", "{{memberCount}} members, {{onlineCount}} online_one": "{{memberCount}} member, {{onlineCount}} online", "{{memberCount}} members, {{onlineCount}} online_other": "{{memberCount}} members, {{onlineCount}} online", - "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} members, {{onlineCount}} online", - "{{count}} unread": "{{count}} unread", - "{{count}} new messages": "{{count}} new messages", - "Unsupported Attachment": "Unsupported Attachment", - "+{{count}} More Options_one": "+{{count}} More Option", - "+{{count}} More Options_other": "+{{count}} More Options", - "+{{count}} More Options_many": "+{{count}} More Options" + "🏙 Attachment...": "🏙 Attachment..." } diff --git a/package/src/i18n/es.json b/package/src/i18n/es.json index c6ebf950d9..fd2dd5cc90 100644 --- a/package/src/i18n/es.json +++ b/package/src/i18n/es.json @@ -1,4 +1,7 @@ { + "+{{count}} More Options_many": "+{{count}} opciones más", + "+{{count}} More Options_one": "+{{count}} opción más", + "+{{count}} More Options_other": "+{{count}} opciones más", "+{{count}}_many": "+{{count}}", "+{{count}}_one": "+{{count}}", "+{{count}}_other": "+{{count}}", @@ -11,8 +14,13 @@ "Allow access to your Gallery": "Permitir acceso a tu galería", "Allow camera access in device settings": "Permitir el acceso a la cámara en la configuración del dispositivo", "Also send to channel": "También enviar al canal", + "Also sent in channel": "También enviado en el canal", "Anonymous": "Anónimo", "Anonymous voting": "Encuesta anónima", + "Archive Chat": "Archivar chat", + "Archive Group": "Archivar grupo", + "Are you sure you want to delete this chat? This can't be undone.": "¿Seguro que quieres eliminar este chat? Esta acción no se puede deshacer.", + "Are you sure you want to delete this group? This can't be undone.": "¿Seguro que quieres eliminar este grupo? Esta acción no se puede deshacer.", "Are you sure you want to permanently delete this message?": "¿Estás seguro de que deseas eliminar permanentemente este mensaje?", "Are you sure?": "¿Estás seguro?", "Ask a question": "Hacer una pregunta", @@ -21,23 +29,32 @@ "Block User": "Bloquear usuario", "Cancel": "Cancelar", "Cannot Flag Message": "No se puede reportar el mensaje", + "Change in Settings": "Cambiar en Ajustes", + "Choose between 2–10 options": "Elige entre 2 y 10 opciones", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Considera cómo tu comentario podría hacer sentir a los demás y asegúrate de seguir nuestras Normas de la Comunidad", "Copy Message": "Copiar mensaje", + "Couldn't load new threads. Tap to retry": "No se pudieron cargar nuevos hilos. Toca para reintentar", "Create Poll": "Crear encuesta", + "Create a poll and let everyone vote": "Crea una encuesta y deja que todos voten", "Delete": "Eliminar", + "Delete Chat": "Eliminar chat", + "Delete Group": "Eliminar grupo", "Delete Message": "Eliminar mensaje", + "Delete chat": "Eliminar chat", "Delete for me": "Eliminar para mí", + "Delete group": "Eliminar grupo", "Device camera is used to take photos or videos.": "La cámara del dispositivo se utiliza para tomar fotografías o vídeos.", "Device gallery permissions is used to take photos or videos.": "Los permisos de la galería del dispositivo se utilizan para tomar fotos o videos.", "Do you want to send a copy of this message to a moderator for further investigation?": "¿Deseas enviar una copia de este mensaje a un moderador para una investigación adicional?", + "Draft": "Borrador", "Due since {{ dueSince }}": "Debido desde {{ dueSince }}", "Edit Message": "Editar mensaje", "Edited": "Editado", "Editing Message": "Editando mensaje", "Emoji matching": "Coincidencia de emoji", "Empty message...": "Mensaje vacío...", - "Enter a new option": "Introduce una nueva opción", "End Vote": "Finalizar votación", + "Enter a new option": "Introduce una nueva opción", "Error loading": "Error al cargar", "Error loading channel list...": "Error al cargar la lista de canales...", "Error loading messages for this channel...": "Error al cargar los mensajes de este canal...", @@ -55,8 +72,11 @@ "Hold to start recording.": "Mantén presionado para comenzar a grabar.", "How about sending your first message to a friend?": "¿Qué tal enviar tu primer mensaje a un amigo?", "Instant Commands": "Comandos instantáneos", + "Leave Chat": "Salir del chat", + "Leave Group": "Salir del grupo", "Let others add options": "Permitir que otros añadan opciones", "Let's start chatting!": "¡Empecemos a charlar!", + "Limit votes per person": "Limita los votos por persona", "Links are disabled": "Los enlaces están desactivados", "Live Location": "Ubicación en vivo", "Loading channels...": "Cargando canales...", @@ -68,24 +88,29 @@ "Maximum number of files reached": "Número máximo de archivos alcanzado", "Message Reactions": "Reacciones al mensaje", "Message deleted": "Mensaje eliminado", + "Message failed to send": "El mensaje no se pudo enviar", "Message flagged": "Mensaje reportado", "Multiple votes": "Votos múltiples", - "Network error": "Error de red", - "Select more than one option": "Selecciona más de una opción", - "Limit votes per person": "Limita los votos por persona", - "Choose between 2–10 options": "Elige entre 2 y 10 opciones", + "Mute Group": "Silenciar grupo", "Mute User": "Silenciar usuario", + "Network error": "Error de red", "No chats here yet…": "No hay chats aquí todavía...", + "No conversations yet": "No hay conversaciones todavía", "No items exist": "No hay elementos", + "No messages yet": "No hay mensajes todavía", "No threads here yet": "Aún no hay hilos aquí", "Not supported": "No admitido", "Nothing yet...": "Aún no hay nada...", + "Offline": "Desconectado", "Ok": "Aceptar", + "Online": "En línea", "Only visible to you": "Solo visible para ti", + "Open Camera": "Abrir camara", + "Open Files": "Abrir archivos", "Open Settings": "Configuración abierta", "Option": "Opción", - "Option {{count}}": "Opción {{count}}", "Option already exists": "La opción ya existe", + "Option {{count}}": "Opción {{count}}", "Options": "Opciones", "Photo": "Foto", "Photos and Videos": "Fotos y videos", @@ -97,16 +122,23 @@ "Poll Comments": "Comentarios de la encuesta", "Poll Options": "Opciones de la encuesta", "Poll Results": "Resultados de la encuesta", + "Poll has ended": "Votación finalizada", "Questions": "Preguntas", "Reconnecting...": "Reconectando...", + "Reminder overdue": "Recordatorio vencido", + "Reminder set": "Recordatorio establecido", + "Replied to a thread": "Respondido a un hilo", "Reply": "Responder", - "Reply to {{name}}": "Responder a {{name}}", "Reply to Message": "Responder al mensaje", + "Reply to a message to start a thread": "Responde a un mensaje para empezar un hilo", + "Reply to {{name}}": "Responder a {{name}}", "Resend": "Reenviar", "Retry Upload": "Reintentar carga", "SEND": "ENVIAR", "Search": "Buscar", "Select More Photos": "Seleccionar más fotos", + "Select files to share": "Selecciona archivos para compartir", + "Select more than one option": "Selecciona más de una opción", "Select one": "Seleccionar una", "Select one or more": "Seleccionar una o más", "Select up to {{count}}_many": "Selecciona hasta {{count}}", @@ -120,29 +152,61 @@ "Slow mode ON": "Modo lento ACTIVADO", "Slow mode, wait {{seconds}}s...": "Modo lento, espera {{seconds}}s...", "Suggest an option": "Sugerir una opción", + "Take a photo and share": "Toma una foto y compártela", + "Take a video and share": "Graba un video y compártelo", + "Tap to remove": "Toca para quitar", "The message has been reported to a moderator.": "El mensaje ha sido reportado a un moderador.", "The source message was deleted": "El mensaje original fue eliminado", "Thinking...": "Pensando...", "This reply was deleted": "Esta respuesta fue eliminada", "Thread Reply": "Respuesta de hilo", "Type a number from 2 to 10": "Escribe un número de 2 a 10", + "Typing": "Escribiendo", + "Unarchive Chat": "Desarchivar chat", + "Unarchive Group": "Desarchivar grupo", "Unban User": "Desbloquear usuario", "Unblock User": "Usuario desconocido", "Unknown User": "Desbloquear Usuario", + "Unmute Group": "Activar sonido del grupo", "Unmute User": "Activar sonido del usuario", "Unpin from Conversation": "Desmarcar de la conversación", "Unread Messages": "Mensajes no leídos", + "Unsupported Attachment": "Adjunto no admitido", "Update your comment": "Actualizar tu comentario", "Video": "Video", + "View": "Ver", "View Results": "Ver resultados", "View {{count}} comments_many": "Ver {{count}} comentarios", "View {{count}} comments_one": "Ver {{count}} comentario", "View {{count}} comments_other": "Ver {{count}} comentarios", "Voice message": "Mensaje de voz", "Voice message ({{duration}})": "Mensaje de voz ({{duration}})", - "Your comment": "Tu comentario", "You": "Tú", "You can't send messages in this channel": "No puedes enviar mensajes en este canal", + "You have not granted access to the photo library.": "No has concedido acceso a la biblioteca de fotos.", + "You have not granted access to your camera": "No has concedido acceso a tu camara", + "You voted: {{ option }}": "Has votado: {{ option }}", + "Your comment": "Tu comentario", + "and {{ count }} others": "y {{ count }} más", + "aria/AI is generating": "AI is generating", + "aria/AI is thinking": "AI is thinking", + "aria/Avatar of {{name}}": "Avatar of {{name}}", + "aria/Connected": "Connected", + "aria/Delivered": "Delivered", + "aria/Loading": "Loading", + "aria/Loading failed": "Loading failed", + "aria/New message from {{user}}": "New message from {{user}}", + "aria/Offline": "Offline", + "aria/Open message actions": "Open message actions", + "aria/Reaction {{emoji}} by {{count}} users": "Reaction {{emoji}} by {{count}} users", + "aria/Read": "Read", + "aria/Reconnecting": "Reconnecting", + "aria/Reply to {{user}}": "Reply to {{user}}", + "aria/Send message": "Send message", + "aria/Sending": "Sending", + "aria/Sent": "Sent", + "aria/Voice message recording. Hold to record.": "Voice message recording. Hold to record.", + "aria/{{count}} new messages": "{{count}} new messages", "duration/Location end at": "{{ milliseconds | durationFormatter(withSuffix: false) }}", "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", @@ -158,16 +222,11 @@ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Ayer]\", \"lastWeek\":\"dddd\", \"nextDay\":\"[Mañana]\", \"nextWeek\":\"dddd [a las] LT\", \"sameDay\":\"LT\", \"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} y {{ nonSelfUserLength }} más están escribiendo", + "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} y {{ secondUser }} están escribiendo", "{{ index }} of {{ photoLength }}": "{{ index }} de {{ photoLength }}", + "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} personas están escribiendo", "{{ replyCount }} Replies": "{{ replyCount }} Respuestas", "{{ user }} is typing": "{{ user }} está escribiendo", - "You voted: {{ option }}": "Has votado: {{ option }}", - "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} y {{ secondUser }} están escribiendo", - "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} personas están escribiendo", - "Typing": "Escribiendo", - "No messages yet": "No hay mensajes todavía", - "Message failed to send": "El mensaje no se pudo enviar", - "and {{ count }} others": "y {{ count }} más", "{{ user }} voted: {{ option }}": "{{ user }} votó: {{ option }}", "{{count}} Audios_many": "{{count}} audios", "{{count}} Audios_one": "{{count}} audio", @@ -178,63 +237,23 @@ "{{count}} Photos_many": "{{count}} fotos", "{{count}} Photos_one": "{{count}} foto", "{{count}} Photos_other": "{{count}} fotos", - "{{count}} Voice messages_many": "{{count}} mensajes de voz", - "{{count}} Voice messages_one": "{{count}} mensaje de voz", - "{{count}} Voice messages_other": "{{count}} mensajes de voz", + "{{count}} Reactions_many": "{{count}} reacciones", + "{{count}} Reactions_one": "{{count}} reacción", + "{{count}} Reactions_other": "{{count}} reacciones", "{{count}} Videos_many": "{{count}} vídeos", "{{count}} Videos_one": "{{count}} vídeo", "{{count}} Videos_other": "{{count}} vídeos", + "{{count}} Voice messages_many": "{{count}} mensajes de voz", + "{{count}} Voice messages_one": "{{count}} mensaje de voz", + "{{count}} Voice messages_other": "{{count}} mensajes de voz", + "{{count}} new messages": "{{count}} nuevos mensajes", + "{{count}} new threads": "{{count}} nuevos hilos", + "{{count}} unread": "{{count}} no leídos", "{{count}} votes_many": "{{count}} votos", "{{count}} votes_one": "{{count}} voto", "{{count}} votes_other": "{{count}} votos", - "🏙 Attachment...": "🏙 Adjunto...", - "You have not granted access to the photo library.": "No has concedido acceso a la biblioteca de fotos.", - "Change in Settings": "Cambiar en Ajustes", - "Create a poll and let everyone vote": "Crea una encuesta y deja que todos voten", - "Open Camera": "Abrir camara", - "Open Files": "Abrir archivos", - "Select files to share": "Selecciona archivos para compartir", - "Take a photo and share": "Toma una foto y compártela", - "Take a video and share": "Graba un video y compártelo", - "You have not granted access to your camera": "No has concedido acceso a tu camara", - "{{count}} Reactions_many": "{{count}} reacciones", - "{{count}} Reactions_one": "{{count}} reacción", - "{{count}} Reactions_other": "{{count}} reacciones", - "Tap to remove": "Toca para quitar", - "Draft": "Borrador", - "Reminder set": "Recordatorio establecido", - "Also sent in channel": "También enviado en el canal", - "Replied to a thread": "Respondido a un hilo", - "View": "Ver", - "Reminder overdue": "Recordatorio vencido", - "Poll has ended": "Votación finalizada", - "Reply to a message to start a thread": "Responde a un mensaje para empezar un hilo", - "Couldn't load new threads. Tap to retry": "No se pudieron cargar nuevos hilos. Toca para reintentar", - "{{count}} new threads": "{{count}} nuevos hilos", - "No conversations yet": "No hay conversaciones todavía", - "Are you sure you want to delete this group? This can't be undone.": "¿Seguro que quieres eliminar este grupo? Esta acción no se puede deshacer.", - "Are you sure you want to delete this chat? This can't be undone.": "¿Seguro que quieres eliminar este chat? Esta acción no se puede deshacer.", - "Delete chat": "Eliminar chat", - "Delete group": "Eliminar grupo", - "Archive Chat": "Archivar chat", - "Archive Group": "Archivar grupo", - "Delete Chat": "Eliminar chat", - "Delete Group": "Eliminar grupo", - "Leave Chat": "Salir del chat", - "Leave Group": "Salir del grupo", - "Mute Group": "Silenciar grupo", - "Offline": "Desconectado", - "Online": "En línea", - "Unarchive Chat": "Desarchivar chat", - "Unarchive Group": "Desarchivar grupo", - "Unmute Group": "Activar sonido del grupo", + "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} miembros, {{onlineCount}} en línea", "{{memberCount}} members, {{onlineCount}} online_one": "{{memberCount}} miembro, {{onlineCount}} en línea", "{{memberCount}} members, {{onlineCount}} online_other": "{{memberCount}} miembros, {{onlineCount}} en línea", - "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} miembros, {{onlineCount}} en línea", - "{{count}} unread": "{{count}} no leídos", - "{{count}} new messages": "{{count}} nuevos mensajes", - "Unsupported Attachment": "Adjunto no admitido", - "+{{count}} More Options_one": "+{{count}} opción más", - "+{{count}} More Options_other": "+{{count}} opciones más", - "+{{count}} More Options_many": "+{{count}} opciones más" + "🏙 Attachment...": "🏙 Adjunto..." } diff --git a/package/src/i18n/fr.json b/package/src/i18n/fr.json index 0ad0522254..6a75184cb2 100644 --- a/package/src/i18n/fr.json +++ b/package/src/i18n/fr.json @@ -1,4 +1,7 @@ { + "+{{count}} More Options_many": "+{{count}} options supplémentaires", + "+{{count}} More Options_one": "+{{count}} option supplémentaire", + "+{{count}} More Options_other": "+{{count}} options supplémentaires", "+{{count}}_many": "+{{count}}", "+{{count}}_one": "+{{count}}", "+{{count}}_other": "+{{count}}", @@ -11,8 +14,13 @@ "Allow access to your Gallery": "Autoriser l'accès à votre galerie", "Allow camera access in device settings": "Autoriser l'accès à la caméra dans les paramètres de l'appareil", "Also send to channel": "Envoyer également à la chaîne", + "Also sent in channel": "También enviado en el canal", "Anonymous": "Anonyme", "Anonymous voting": "Sondage anonyme", + "Archive Chat": "Archiver la discussion", + "Archive Group": "Archiver le groupe", + "Are you sure you want to delete this chat? This can't be undone.": "Êtes-vous sûr de vouloir supprimer cette discussion ? Cette action est irréversible.", + "Are you sure you want to delete this group? This can't be undone.": "Êtes-vous sûr de vouloir supprimer ce groupe ? Cette action est irréversible.", "Are you sure you want to permanently delete this message?": "Êtes-vous sûr de vouloir supprimer définitivement ce message?", "Are you sure?": "Es-tu sûr ?", "Ask a question": "Poser une question", @@ -21,23 +29,32 @@ "Block User": "Bloquer un utilisateur", "Cancel": "Annuler", "Cannot Flag Message": "Impossible de signaler le message", + "Change in Settings": "Changer dans Réglages", + "Choose between 2–10 options": "Choisissez entre 2 et 10 options", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Considérez comment votre commentaire pourrait faire sentir les autres et assurez-vous de suivre nos directives communautaires", "Copy Message": "Copier le message", + "Couldn't load new threads. Tap to retry": "Impossible de charger les nouveaux fils. Appuyez pour réessayer", "Create Poll": "Créer un sondage", + "Create a poll and let everyone vote": "Créez un sondage et laissez tout le monde voter", "Delete": "Supprimer", + "Delete Chat": "Supprimer la discussion", + "Delete Group": "Supprimer le groupe", "Delete Message": "Supprimer un message", + "Delete chat": "Supprimer la discussion", "Delete for me": "Supprimer pour moi", + "Delete group": "Supprimer le groupe", "Device camera is used to take photos or videos.": "L'appareil photo de l'appareil est utilisé pour prendre des photos ou des vidéos.", "Device gallery permissions is used to take photos or videos.": "Les autorisations de la galerie de l'appareil sont utilisées pour prendre des photos ou des vidéos.", "Do you want to send a copy of this message to a moderator for further investigation?": "Voulez-vous envoyer une copie de ce message à un modérateur pour une enquête plus approfondie?", + "Draft": "Brouillon", "Due since {{ dueSince }}": "Échéance depuis {{ dueSince }}", "Edit Message": "Éditer un message", "Edited": "Édité", "Editing Message": "Édite un message", "Emoji matching": "Correspondance Emoji", "Empty message...": "Message vide...", - "Enter a new option": "Saisissez une nouvelle option", "End Vote": "Fin du vote", + "Enter a new option": "Saisissez une nouvelle option", "Error loading": "Erreur lors du chargement", "Error loading channel list...": "Erreur lors du chargement de la liste de canaux...", "Error loading messages for this channel...": "Erreur lors du chargement des messages de ce canal...", @@ -55,8 +72,11 @@ "Hold to start recording.": "Hold to start recording.", "How about sending your first message to a friend?": "Et si vous envoyiez votre premier message à un ami ?", "Instant Commands": "Commandes Instantanées", + "Leave Chat": "Quitter la discussion", + "Leave Group": "Quitter le groupe", "Let others add options": "Autoriser d'autres à ajouter des options", "Let's start chatting!": "Commençons à discuter !", + "Limit votes per person": "Limiter les votes par personne", "Links are disabled": "Links are disabled", "Live Location": "Position en direct", "Loading channels...": "Chargement des canaux...", @@ -68,24 +88,29 @@ "Maximum number of files reached": "Nombre maximal de fichiers atteint", "Message Reactions": "Réactions aux messages", "Message deleted": "Message supprimé", + "Message failed to send": "Le message n'a pas pu être envoyé", "Message flagged": "Message signalé", "Multiple votes": "Votes multiples", - "Network error": "Erreur réseau", - "Select more than one option": "Sélectionnez plus d’une option", - "Limit votes per person": "Limiter les votes par personne", - "Choose between 2–10 options": "Choisissez entre 2 et 10 options", + "Mute Group": "Mettre le groupe en sourdine", "Mute User": "Utilisateur muet", + "Network error": "Erreur réseau", "No chats here yet…": "Pas de discussions ici pour le moment…", + "No conversations yet": "Aucune conversation pour le moment", "No items exist": "Aucun élément", + "No messages yet": "Aucun message pour le moment", "No threads here yet": "Aucun fil ici pour le moment", "Not supported": "Non pris en charge", "Nothing yet...": "Aucun message...", + "Offline": "Hors ligne", "Ok": "Ok", + "Online": "En ligne", "Only visible to you": "Seulement visible par vous", + "Open Camera": "Ouvrir la caméra", + "Open Files": "Ouvrir les fichiers", "Open Settings": "Ouvrir les paramètres", "Option": "Option", - "Option {{count}}": "Option {{count}}", "Option already exists": "L'option existe déjà", + "Option {{count}}": "Option {{count}}", "Options": "Options", "Photo": "Photo", "Photos and Videos": "Photos et vidéos", @@ -97,16 +122,23 @@ "Poll Comments": "Commentaires du sondage", "Poll Options": "Options du sondage", "Poll Results": "Résultats du sondage", + "Poll has ended": "Vote terminé", "Questions": "Questions", "Reconnecting...": "Se Reconnecter...", + "Reminder overdue": "Recordatorio vencido", + "Reminder set": "Recordatorio establecido", + "Replied to a thread": "Respondido a un hilo", "Reply": "Répondre", - "Reply to {{name}}": "Répondre à {{name}}", "Reply to Message": "Répondre au message", + "Reply to a message to start a thread": "Répondre à un message pour commencer un fil", + "Reply to {{name}}": "Répondre à {{name}}", "Resend": "Renvoyer", "Retry Upload": "Réessayer l'envoi", "SEND": "ENVOYER", "Search": "Rechercher", "Select More Photos": "Sélectionner plus de photos", + "Select files to share": "Sélectionnez des fichiers à partager", + "Select more than one option": "Sélectionnez plus d’une option", "Select one": "Sélectionner une", "Select one or more": "Sélectionner une ou plusieurs", "Select up to {{count}}_many": "Sélectionnez jusqu'à {{count}}", @@ -120,29 +152,61 @@ "Slow mode ON": "Mode lent activé", "Slow mode, wait {{seconds}}s...": "Mode lent, attendez {{seconds}}s...", "Suggest an option": "Suggérer une option", + "Take a photo and share": "Prenez une photo et partagez-la", + "Take a video and share": "Prenez une vidéo et partagez-la", + "Tap to remove": "Appuyez pour retirer", "The message has been reported to a moderator.": "Le message a été signalé à un modérateur.", "The source message was deleted": "Le message source a été supprimé", "Thinking...": "Réflexion...", "This reply was deleted": "Cette réponse a été supprimée", "Thread Reply": "Réponse à la discussion", "Type a number from 2 to 10": "Entrez un nombre de 2 à 10", + "Typing": "Écrivant", + "Unarchive Chat": "Désarchiver la discussion", + "Unarchive Group": "Désarchiver le groupe", "Unban User": "Débannir Utilisateur", "Unblock User": "Débloquer Utilisateur", "Unknown User": "Utilisateur inconnu", + "Unmute Group": "Rétablir le son du groupe", "Unmute User": "Activer le son de Utilisateur", "Unpin from Conversation": "Décrocher de la conversation", "Unread Messages": "Messages non lus", + "Unsupported Attachment": "Pièce jointe non prise en charge", "Update your comment": "Mettre à jour votre commentaire", "Video": "Vidéo", + "View": "Ver", "View Results": "Voir les résultats", "View {{count}} comments_many": "Voir {{count}} commentaires", "View {{count}} comments_one": "Voir {{count}} commentaire", "View {{count}} comments_other": "Voir {{count}} commentaires", "Voice message": "Message vocal", "Voice message ({{duration}})": "Message vocal ({{duration}})", - "Your comment": "Votre commentaire", "You": "Toi", "You can't send messages in this channel": "You can't send messages in this channel", + "You have not granted access to the photo library.": "Vous n'avez pas accordé l'accès à la photothèque.", + "You have not granted access to your camera": "Vous n'avez pas accordé l'accès à votre caméra", + "You voted: {{ option }}": "Vous avez voté: {{ option }}", + "Your comment": "Votre commentaire", + "and {{ count }} others": "et {{ count }} autres", + "aria/AI is generating": "AI is generating", + "aria/AI is thinking": "AI is thinking", + "aria/Avatar of {{name}}": "Avatar of {{name}}", + "aria/Connected": "Connected", + "aria/Delivered": "Delivered", + "aria/Loading": "Loading", + "aria/Loading failed": "Loading failed", + "aria/New message from {{user}}": "New message from {{user}}", + "aria/Offline": "Offline", + "aria/Open message actions": "Open message actions", + "aria/Reaction {{emoji}} by {{count}} users": "Reaction {{emoji}} by {{count}} users", + "aria/Read": "Read", + "aria/Reconnecting": "Reconnecting", + "aria/Reply to {{user}}": "Reply to {{user}}", + "aria/Send message": "Send message", + "aria/Sending": "Sending", + "aria/Sent": "Sent", + "aria/Voice message recording. Hold to record.": "Voice message recording. Hold to record.", + "aria/{{count}} new messages": "{{count}} new messages", "duration/Location end at": "{{ milliseconds | durationFormatter(withSuffix: false) }}", "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", @@ -158,16 +222,11 @@ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Hier]\", \"lastWeek\":\"dddd\", \"nextDay\":\"[Demain]\", \"nextWeek\":\"dddd [à] LT\", \"sameDay\":\"LT\", \"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} et {{ nonSelfUserLength }} autres sont en train d'écrire", + "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} et {{ secondUser }} sont en train d'écrire", "{{ index }} of {{ photoLength }}": "{{ index }} sur {{ photoLength }}", + "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} personnes sont en train d'écrire", "{{ replyCount }} Replies": "{{ replyCount }} Réponses", "{{ user }} is typing": "{{ user }} est en train d'écrire", - "You voted: {{ option }}": "Vous avez voté: {{ option }}", - "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} et {{ secondUser }} sont en train d'écrire", - "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} personnes sont en train d'écrire", - "Typing": "Écrivant", - "No messages yet": "Aucun message pour le moment", - "Message failed to send": "Le message n'a pas pu être envoyé", - "and {{ count }} others": "et {{ count }} autres", "{{ user }} voted: {{ option }}": "{{ user }} a voté: {{ option }}", "{{count}} Audios_many": "{{count}} audios", "{{count}} Audios_one": "{{count}} audio", @@ -178,63 +237,23 @@ "{{count}} Photos_many": "{{count}} photos", "{{count}} Photos_one": "{{count}} photo", "{{count}} Photos_other": "{{count}} photos", - "{{count}} Voice messages_many": "{{count}} messages vocaux", - "{{count}} Voice messages_one": "{{count}} message vocal", - "{{count}} Voice messages_other": "{{count}} messages vocaux", + "{{count}} Reactions_many": "{{count}} réactions", + "{{count}} Reactions_one": "{{count}} réaction", + "{{count}} Reactions_other": "{{count}} réactions", "{{count}} Videos_many": "{{count}} vidéos", "{{count}} Videos_one": "{{count}} vidéo", "{{count}} Videos_other": "{{count}} vidéos", + "{{count}} Voice messages_many": "{{count}} messages vocaux", + "{{count}} Voice messages_one": "{{count}} message vocal", + "{{count}} Voice messages_other": "{{count}} messages vocaux", + "{{count}} new messages": "{{count}} nouveaux messages", + "{{count}} new threads": "{{count}} nouveaux fils", + "{{count}} unread": "{{count}} non lus", "{{count}} votes_many": "{{count}} votes", "{{count}} votes_one": "{{count}} vote", "{{count}} votes_other": "{{count}} votes", - "🏙 Attachment...": "🏙 Pièce jointe...", - "You have not granted access to the photo library.": "Vous n'avez pas accordé l'accès à la photothèque.", - "Change in Settings": "Changer dans Réglages", - "Create a poll and let everyone vote": "Créez un sondage et laissez tout le monde voter", - "Open Camera": "Ouvrir la caméra", - "Open Files": "Ouvrir les fichiers", - "Select files to share": "Sélectionnez des fichiers à partager", - "Take a photo and share": "Prenez une photo et partagez-la", - "Take a video and share": "Prenez une vidéo et partagez-la", - "You have not granted access to your camera": "Vous n'avez pas accordé l'accès à votre caméra", - "{{count}} Reactions_many": "{{count}} réactions", - "{{count}} Reactions_one": "{{count}} réaction", - "{{count}} Reactions_other": "{{count}} réactions", - "Tap to remove": "Appuyez pour retirer", - "Draft": "Brouillon", - "Reminder set": "Recordatorio establecido", - "Also sent in channel": "También enviado en el canal", - "Replied to a thread": "Respondido a un hilo", - "View": "Ver", - "Reminder overdue": "Recordatorio vencido", - "Poll has ended": "Vote terminé", - "Reply to a message to start a thread": "Répondre à un message pour commencer un fil", - "Couldn't load new threads. Tap to retry": "Impossible de charger les nouveaux fils. Appuyez pour réessayer", - "{{count}} new threads": "{{count}} nouveaux fils", - "No conversations yet": "Aucune conversation pour le moment", - "Are you sure you want to delete this group? This can't be undone.": "Êtes-vous sûr de vouloir supprimer ce groupe ? Cette action est irréversible.", - "Are you sure you want to delete this chat? This can't be undone.": "Êtes-vous sûr de vouloir supprimer cette discussion ? Cette action est irréversible.", - "Delete chat": "Supprimer la discussion", - "Delete group": "Supprimer le groupe", - "Archive Chat": "Archiver la discussion", - "Archive Group": "Archiver le groupe", - "Delete Chat": "Supprimer la discussion", - "Delete Group": "Supprimer le groupe", - "Leave Chat": "Quitter la discussion", - "Leave Group": "Quitter le groupe", - "Mute Group": "Mettre le groupe en sourdine", - "Offline": "Hors ligne", - "Online": "En ligne", - "Unarchive Chat": "Désarchiver la discussion", - "Unarchive Group": "Désarchiver le groupe", - "Unmute Group": "Rétablir le son du groupe", + "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} membres, {{onlineCount}} en ligne", "{{memberCount}} members, {{onlineCount}} online_one": "{{memberCount}} membre, {{onlineCount}} en ligne", "{{memberCount}} members, {{onlineCount}} online_other": "{{memberCount}} membres, {{onlineCount}} en ligne", - "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} membres, {{onlineCount}} en ligne", - "{{count}} unread": "{{count}} non lus", - "{{count}} new messages": "{{count}} nouveaux messages", - "Unsupported Attachment": "Pièce jointe non prise en charge", - "+{{count}} More Options_one": "+{{count}} option supplémentaire", - "+{{count}} More Options_other": "+{{count}} options supplémentaires", - "+{{count}} More Options_many": "+{{count}} options supplémentaires" + "🏙 Attachment...": "🏙 Pièce jointe..." } diff --git a/package/src/i18n/he.json b/package/src/i18n/he.json index 6611b5aa45..6e42273891 100644 --- a/package/src/i18n/he.json +++ b/package/src/i18n/he.json @@ -1,4 +1,7 @@ { + "+{{count}} More Options_many": "+עוד {{count}} אפשרויות", + "+{{count}} More Options_one": "+עוד אפשרות {{count}}", + "+{{count}} More Options_other": "+עוד {{count}} אפשרויות", "+{{count}}_many": "+{{count}}", "+{{count}}_one": "+{{count}}", "+{{count}}_other": "+{{count}}", @@ -11,8 +14,13 @@ "Allow access to your Gallery": "אפשר גישה לגלריה שלך", "Allow camera access in device settings": "אפשר גישה למצלמה בהגדרות המכשיר", "Also send to channel": "שלח/י הודעה לשיחה", + "Also sent in channel": "שלח/י גם לשיחה", "Anonymous": "אנונימי", "Anonymous voting": "סקר אנונימי", + "Archive Chat": "העבר/י צ׳אט לארכיון", + "Archive Group": "העבר/י קבוצה לארכיון", + "Are you sure you want to delete this chat? This can't be undone.": "האם למחוק את הצ׳אט הזה? לא ניתן לבטל את הפעולה.", + "Are you sure you want to delete this group? This can't be undone.": "האם למחוק את הקבוצה הזו? לא ניתן לבטל את הפעולה.", "Are you sure you want to permanently delete this message?": "האם את/ה בטוח/ה שאת/ה רוצה למחוק את ההודעה הזו לצמיתות?", "Are you sure?": "האם אתה בטוח?", "Ask a question": "שאל שאלה", @@ -21,23 +29,32 @@ "Block User": "חסום משתמש", "Cancel": "ביטול", "Cannot Flag Message": "סימון הודעה לא אפשרי", + "Change in Settings": "שנה בהגדרות", + "Choose between 2–10 options": "בחר/י בין 2 ל-10 אפשרויות", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "שקול איך התגובה שלך עשויה להשפיע על אחרים ווודא שאתה עוקב אחר ההנחיות של הקהילה שלנו", "Copy Message": "העתק/י הודעה", + "Couldn't load new threads. Tap to retry": "לא ניתן לטעון שרשורים חדשים. הקש כדי לנסות שוב", "Create Poll": "צור סקר", + "Create a poll and let everyone vote": "צור סקר ותן לכולם להצביע", "Delete": "מחק", + "Delete Chat": "מחק/י צ׳אט", + "Delete Group": "מחק/י קבוצה", "Delete Message": "מחק/י הודעה", + "Delete chat": "מחק/י צ׳אט", "Delete for me": "מחק עבורי", + "Delete group": "מחק/י קבוצה", "Device camera is used to take photos or videos.": "מצלמת המכשיר משמשת לצילום תמונות או סרטונים.", "Device gallery permissions is used to take photos or videos.": "הרשאות גלריית המכשיר משמשות לצילום תמונות או סרטונים.", "Do you want to send a copy of this message to a moderator for further investigation?": "האם את/ה רוצה לשלוח עותק של הודעה זו למנחה להמשך חקירה?", + "Draft": "טיוטה", "Due since {{ dueSince }}": "מועד אחרון מאז {{ dueSince }}", "Edit Message": "ערוך הודעה", "Edited": "נערך", "Editing Message": "הודעה בעריכה", "Emoji matching": "התאמת אמוג'י", "Empty message...": "הודעה ריקה...", - "Enter a new option": "הזן אפשרות חדשה", "End Vote": "סיים הצבעה", + "Enter a new option": "הזן אפשרות חדשה", "Error loading": "שגיאה ארעה בעת הטעינה", "Error loading channel list...": "שגיאה ארעה בטעינת השיחות...", "Error loading messages for this channel...": "שגיאה ארעה בטעינת הודעות עבור שיחה זאת...", @@ -55,8 +72,11 @@ "Hold to start recording.": "לחץ והחזק כדי להתחיל להקליט.", "How about sending your first message to a friend?": "מה דעתך לשלוח את ההודעה הראשונה שלך לחבר?", "Instant Commands": "פעולות מיידיות", + "Leave Chat": "צא/י מהצ׳אט", + "Leave Group": "צא/י מהקבוצה", "Let others add options": "אפשר לאחרים להוסיף אפשרויות", "Let's start chatting!": "בואו נתחיל לשוחח!", + "Limit votes per person": "הגבל/י את מספר ההצבעות לאדם", "Links are disabled": "הקישורים מבוטלים", "Live Location": "מיקום חי", "Loading channels...": "השיחות בטעינה...", @@ -68,24 +88,29 @@ "Maximum number of files reached": "הגעת למספר המרבי של קבצים", "Message Reactions": "תגובות להודעה", "Message deleted": "ההודעה נמחקה", + "Message failed to send": "ההודעה לא נשלחה", "Message flagged": "ההודעה סומנה", "Multiple votes": "הצבעות מרובות", - "Network error": "שגיאת רשת", - "Select more than one option": "בחר/י יותר מאפשרות אחת", - "Limit votes per person": "הגבל/י את מספר ההצבעות לאדם", - "Choose between 2–10 options": "בחר/י בין 2 ל-10 אפשרויות", + "Mute Group": "השתק/י קבוצה", "Mute User": "השתק/י משתמש", + "Network error": "שגיאת רשת", "No chats here yet…": "אין צ'אטים כאן עדיין...", + "No conversations yet": "אין שיחות עדיין", "No items exist": "אין פריטים", + "No messages yet": "אין הודעות עדיין", "No threads here yet": "אין שרשורים כאן עדיין", "Not supported": "לא נתמך", "Nothing yet...": "אינפורמציה תתקבל בהמשך...", + "Offline": "לא מחובר/ת", "Ok": "אוקיי", + "Online": "מחובר/ת", "Only visible to you": "גלוי רק לך", + "Open Camera": "פתח מצלמה", + "Open Files": "פתח קבצים", "Open Settings": "פתח את ההגדרות", "Option": "אפשרות", - "Option {{count}}": "אפשרות {{count}}", "Option already exists": "האפשרות כבר קיימת", + "Option {{count}}": "אפשרות {{count}}", "Options": "אפשרויות", "Photo": "תמונה", "Photos and Videos": "תמונות ווידאו", @@ -97,16 +122,23 @@ "Poll Comments": "תגובות לסקר", "Poll Options": "אפשרויות הסקר", "Poll Results": "תוצאות הסקר", + "Poll has ended": "ההצבעה הסתיימה", "Questions": "שאלות", "Reconnecting...": "מתחבר מחדש...", + "Reminder overdue": "הזמן פג", + "Reminder set": "הזמן הוקם", + "Replied to a thread": "הגב/י בשרשור", "Reply": "השב/י", - "Reply to {{name}}": "השב/י ל-{{name}}", "Reply to Message": "השב/י להודעה", + "Reply to a message to start a thread": "השב/י להודעה כדי להתחיל שרשור", + "Reply to {{name}}": "השב/י ל-{{name}}", "Resend": "שלח/י שוב", "Retry Upload": "נסה להעלות שוב", "SEND": "שלח", "Search": "חפש/י", "Select More Photos": "בחר עוד תמונות", + "Select files to share": "בחר קבצים לשיתוף", + "Select more than one option": "בחר/י יותר מאפשרות אחת", "Select one": "בחר אחת", "Select one or more": "בחר אחת או יותר", "Select up to {{count}}_many": "בחר עד {{count}}", @@ -120,29 +152,61 @@ "Slow mode ON": "מצב איטי מופעל", "Slow mode, wait {{seconds}}s...": "מצב איטי, חכה {{seconds}}s...", "Suggest an option": "הצע אפשרות", + "Take a photo and share": "צלם תמונה ושתף", + "Take a video and share": "צלם וידאו ושתף", + "Tap to remove": "הקש כדי להסיר", "The message has been reported to a moderator.": "ההודעה דווחה למנהל", "The source message was deleted": "ההודעה המקורית נמחקה", "Thinking...": "חושב...", "This reply was deleted": "התגובה הזו נמחקה", "Thread Reply": "הגב/י בשרשור", "Type a number from 2 to 10": "הקלד מספר בין 2 ל-10", + "Typing": "מקליד/ה", + "Unarchive Chat": "הוצא/י צ׳אט מהארכיון", + "Unarchive Group": "הוצא/י קבוצה מהארכיון", "Unban User": "לבטל חסימת משתמש", "Unblock User": "בטל/י חסימת משתמש", "Unknown User": "משתמש לא ידוע", + "Unmute Group": "בטל/י השתקת קבוצה", "Unmute User": "בטל/י השתקת משתמש", "Unpin from Conversation": "בטל/י הצמדה לשיחה", "Unread Messages": "הודעות שטרם נקרו", + "Unsupported Attachment": "קובץ לא נתמך", "Update your comment": "עדכן את התגובה שלך", "Video": "וִידֵאוֹ", + "View": "צפה", "View Results": "הצג תוצאות", "View {{count}} comments_many": "הצג {{count}} תגובות", "View {{count}} comments_one": "הצג {{count}} תגובה", "View {{count}} comments_other": "הצג {{count}} תגובות", "Voice message": "הודעת קול", "Voice message ({{duration}})": "הודעת קול ({{duration}})", - "Your comment": "התגובה שלך", "You": "את/ה", "You can't send messages in this channel": "את/ב לא יכול/ה לשלוח הודעות בשיחה זו", + "You have not granted access to the photo library.": "לא הענקת גישה לספריית התמונות.", + "You have not granted access to your camera": "לא הענקת גישה למצלמה שלך", + "You voted: {{ option }}": "הצבעת: {{ option }}", + "Your comment": "התגובה שלך", + "and {{ count }} others": "ועוד {{ count }} משתמש/ים", + "aria/AI is generating": "AI is generating", + "aria/AI is thinking": "AI is thinking", + "aria/Avatar of {{name}}": "Avatar of {{name}}", + "aria/Connected": "Connected", + "aria/Delivered": "Delivered", + "aria/Loading": "Loading", + "aria/Loading failed": "Loading failed", + "aria/New message from {{user}}": "New message from {{user}}", + "aria/Offline": "Offline", + "aria/Open message actions": "Open message actions", + "aria/Reaction {{emoji}} by {{count}} users": "Reaction {{emoji}} by {{count}} users", + "aria/Read": "Read", + "aria/Reconnecting": "Reconnecting", + "aria/Reply to {{user}}": "Reply to {{user}}", + "aria/Send message": "Send message", + "aria/Sending": "Sending", + "aria/Sent": "Sent", + "aria/Voice message recording. Hold to record.": "Voice message recording. Hold to record.", + "aria/{{count}} new messages": "{{count}} new messages", "duration/Location end at": "{{ milliseconds | durationFormatter(withSuffix: false) }}", "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", @@ -158,16 +222,11 @@ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[אתמול]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[מחר]\",\"nextWeek\":\"dddd [בשעה] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} ו-{{ nonSelfUserLength }} משתמש/ים אחר/ים מקלידים", + "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} ו-{{ secondUser }} מקלידים", "{{ index }} of {{ photoLength }}": "{{ index }} מתוך {{ photoLength }}", + "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} משתמש/ים מקלידים", "{{ replyCount }} Replies": "{{ replyCount }} תגובות", "{{ user }} is typing": "{{ user }} מקליד/ה", - "You voted: {{ option }}": "הצבעת: {{ option }}", - "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} ו-{{ secondUser }} מקלידים", - "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} משתמש/ים מקלידים", - "Typing": "מקליד/ה", - "No messages yet": "אין הודעות עדיין", - "Message failed to send": "ההודעה לא נשלחה", - "and {{ count }} others": "ועוד {{ count }} משתמש/ים", "{{ user }} voted: {{ option }}": "{{ user }} הצבע: {{ option }}", "{{count}} Audios_many": "{{count}} קבצי אודיו", "{{count}} Audios_one": "{{count}} קובץ אודיו", @@ -178,63 +237,23 @@ "{{count}} Photos_many": "{{count}} תמונות", "{{count}} Photos_one": "{{count}} תמונה", "{{count}} Photos_other": "{{count}} תמונות", - "{{count}} Voice messages_many": "{{count}} הודעות קול", - "{{count}} Voice messages_one": "{{count}} הודעת קול", - "{{count}} Voice messages_other": "{{count}} הודעות קול", + "{{count}} Reactions_many": "{{count}} תגובות", + "{{count}} Reactions_one": "{{count}} תגובה", + "{{count}} Reactions_other": "{{count}} תגובות", "{{count}} Videos_many": "{{count}} סרטונים", "{{count}} Videos_one": "{{count}} סרטון", "{{count}} Videos_other": "{{count}} סרטונים", + "{{count}} Voice messages_many": "{{count}} הודעות קול", + "{{count}} Voice messages_one": "{{count}} הודעת קול", + "{{count}} Voice messages_other": "{{count}} הודעות קול", + "{{count}} new messages": "{{count}} הודעות חדשות", + "{{count}} new threads": "{{count}} שרשורים חדשים", + "{{count}} unread": "{{count}} שטרם נקרא", "{{count}} votes_many": "{{count}} הצבעות", "{{count}} votes_one": "{{count}} הצבעה", "{{count}} votes_other": "{{count}} הצבעות", - "🏙 Attachment...": "🏙 קובץ מצורף...", - "You have not granted access to the photo library.": "לא הענקת גישה לספריית התמונות.", - "Change in Settings": "שנה בהגדרות", - "Create a poll and let everyone vote": "צור סקר ותן לכולם להצביע", - "Open Camera": "פתח מצלמה", - "Open Files": "פתח קבצים", - "Select files to share": "בחר קבצים לשיתוף", - "Take a photo and share": "צלם תמונה ושתף", - "Take a video and share": "צלם וידאו ושתף", - "You have not granted access to your camera": "לא הענקת גישה למצלמה שלך", - "{{count}} Reactions_many": "{{count}} תגובות", - "{{count}} Reactions_one": "{{count}} תגובה", - "{{count}} Reactions_other": "{{count}} תגובות", - "Tap to remove": "הקש כדי להסיר", - "Draft": "טיוטה", - "Reminder set": "הזמן הוקם", - "Also sent in channel": "שלח/י גם לשיחה", - "Replied to a thread": "הגב/י בשרשור", - "View": "צפה", - "Reminder overdue": "הזמן פג", - "Poll has ended": "ההצבעה הסתיימה", - "Reply to a message to start a thread": "השב/י להודעה כדי להתחיל שרשור", - "Couldn't load new threads. Tap to retry": "לא ניתן לטעון שרשורים חדשים. הקש כדי לנסות שוב", - "{{count}} new threads": "{{count}} שרשורים חדשים", - "No conversations yet": "אין שיחות עדיין", - "Are you sure you want to delete this group? This can't be undone.": "האם למחוק את הקבוצה הזו? לא ניתן לבטל את הפעולה.", - "Are you sure you want to delete this chat? This can't be undone.": "האם למחוק את הצ׳אט הזה? לא ניתן לבטל את הפעולה.", - "Delete chat": "מחק/י צ׳אט", - "Delete group": "מחק/י קבוצה", - "Archive Chat": "העבר/י צ׳אט לארכיון", - "Archive Group": "העבר/י קבוצה לארכיון", - "Delete Chat": "מחק/י צ׳אט", - "Delete Group": "מחק/י קבוצה", - "Leave Chat": "צא/י מהצ׳אט", - "Leave Group": "צא/י מהקבוצה", - "Mute Group": "השתק/י קבוצה", - "Offline": "לא מחובר/ת", - "Online": "מחובר/ת", - "Unarchive Chat": "הוצא/י צ׳אט מהארכיון", - "Unarchive Group": "הוצא/י קבוצה מהארכיון", - "Unmute Group": "בטל/י השתקת קבוצה", + "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} חברים, {{onlineCount}} מחוברים", "{{memberCount}} members, {{onlineCount}} online_one": "{{memberCount}} חבר/ה, {{onlineCount}} מחוברים", "{{memberCount}} members, {{onlineCount}} online_other": "{{memberCount}} חברים, {{onlineCount}} מחוברים", - "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} חברים, {{onlineCount}} מחוברים", - "{{count}} unread": "{{count}} שטרם נקרא", - "{{count}} new messages": "{{count}} הודעות חדשות", - "Unsupported Attachment": "קובץ לא נתמך", - "+{{count}} More Options_one": "+עוד אפשרות {{count}}", - "+{{count}} More Options_other": "+עוד {{count}} אפשרויות", - "+{{count}} More Options_many": "+עוד {{count}} אפשרויות" + "🏙 Attachment...": "🏙 קובץ מצורף..." } diff --git a/package/src/i18n/hi.json b/package/src/i18n/hi.json index 6bbd367d23..074c18f2d3 100644 --- a/package/src/i18n/hi.json +++ b/package/src/i18n/hi.json @@ -1,4 +1,7 @@ { + "+{{count}} More Options_many": "+{{count}} और विकल्प", + "+{{count}} More Options_one": "+{{count}} और विकल्प", + "+{{count}} More Options_other": "+{{count}} और विकल्प", "+{{count}}_many": "+{{count}}", "+{{count}}_one": "+{{count}}", "+{{count}}_other": "+{{count}}", @@ -11,8 +14,13 @@ "Allow access to your Gallery": "अपनी गैलरी तक पहुँचने की अनुमति दें", "Allow camera access in device settings": "डिवाइस सेटिंग्स में कैमरा एक्सेस की अनुमति दें", "Also send to channel": "चैनल को भी भेजें", + "Also sent in channel": "चैनल में भी भेजा गया", "Anonymous": "गुमनाम", "Anonymous voting": "अनाम सर्वेक्षण", + "Archive Chat": "चैट संग्रहित करें", + "Archive Group": "ग्रुप संग्रहित करें", + "Are you sure you want to delete this chat? This can't be undone.": "क्या आप वाकई इस चैट को हटाना चाहते हैं? यह वापस नहीं किया जा सकता।", + "Are you sure you want to delete this group? This can't be undone.": "क्या आप वाकई इस ग्रुप को हटाना चाहते हैं? यह वापस नहीं किया जा सकता।", "Are you sure you want to permanently delete this message?": "क्या आप वाकई इस संदेश को स्थायी रूप से हटाना चाहते हैं?", "Are you sure?": "क्या आप सुनिश्चित हैं?", "Ask a question": "एक प्रश्न पूछें", @@ -21,23 +29,32 @@ "Block User": "उपयोगकर्ता को रोक देना, ब्लॉक यूजर", "Cancel": "रद्द करें", "Cannot Flag Message": "मैसेज फ्लैग नहीं किया जा सकता है", + "Change in Settings": "सेटिंग्स में बदलें", + "Choose between 2–10 options": "2–10 विकल्प चुनें", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "ध्यान दें कि आपका संदेश दूसरों को कैसा लगा सकता है और सुनिश्चित हों कि आप हमारी सामुदायिक अनुशासन का पालन कर रहे हैं", "Copy Message": "संदेश की प्रतिलिपि बनाएँ", + "Couldn't load new threads. Tap to retry": "नये थ्रेड्स लोड नहीं हो सके। टैप करके पुनः कोशिश करें", "Create Poll": "सर्वेक्षण बनाएं", + "Create a poll and let everyone vote": "पोल बनाएँ और सभी को वोट करने दें", "Delete": "हटाएं", + "Delete Chat": "चैट हटाएं", + "Delete Group": "ग्रुप हटाएं", "Delete Message": "मैसेज को डिलीट करे", + "Delete chat": "चैट हटाएं", "Delete for me": "मुझे हटाएं", + "Delete group": "ग्रुप हटाएं", "Device camera is used to take photos or videos.": "डिवाइस कैमरे का उपयोग फ़ोटो या वीडियो लेने के लिए किया जाता है।", "Device gallery permissions is used to take photos or videos.": "डिवाइस गैलरी की अनुमतियों का उपयोग फोटो या वीडियो लेने के लिए किया जाता है।", "Do you want to send a copy of this message to a moderator for further investigation?": "क्या आप इस संदेश की एक प्रति आगे की जाँच के लिए किसी मॉडरेटर को भेजना चाहते हैं?", + "Draft": "ड्राफ्ट", "Due since {{ dueSince }}": "{{ dueSince }} से देय है", "Edit Message": "मैसेज में बदलाव करे", "Edited": "मैसेज बदला गया है", "Editing Message": "मैसेज बदला जा रहा है", "Emoji matching": "इमोजी मिलान", "Empty message...": "खाली संदेश...", - "Enter a new option": "एक नया विकल्प दर्ज करें", "End Vote": "वोट समाप्त करें", + "Enter a new option": "एक नया विकल्प दर्ज करें", "Error loading": "लोड होने मे त्रुटि", "Error loading channel list...": "चैनल सूची लोड करने में त्रुटि...", "Error loading messages for this channel...": "इस चैनल के लिए मेसेजेस लोड करने में त्रुटि हुई...", @@ -55,8 +72,11 @@ "Hold to start recording.": "रिकॉर्डिंग शुरू करने के लिए दबाएं।", "How about sending your first message to a friend?": "किसी मित्र को अपना पहला संदेश भेजने के बारे में क्या ख़याल है?", "Instant Commands": "त्वरित कमांड", + "Leave Chat": "चैट छोड़ें", + "Leave Group": "ग्रुप छोड़ें", "Let others add options": "दूसरों को विकल्प जोड़ने दें", "Let's start chatting!": "आइए चैट करना शुरू करें!", + "Limit votes per person": "प्रति व्यक्ति वोट सीमित करें", "Links are disabled": "लिंक अक्षम हैं", "Live Location": "लाइव लोकेशन", "Loading channels...": "चैनल लोड हो रहे हैं...", @@ -68,24 +88,29 @@ "Maximum number of files reached": "फ़ाइलों की अधिकतम संख्या पहुँच गई", "Message Reactions": "संदेश प्रतिक्रियाएँ", "Message deleted": "मैसेज हटा दिया गया", + "Message failed to send": "मैसेज भेजने में विफल", "Message flagged": "संदेश को ध्वजांकित किया गया", "Multiple votes": "एकाधिक वोट", - "Network error": "नेटवर्क त्रुटि", - "Select more than one option": "एक से अधिक विकल्प चुनें", - "Limit votes per person": "प्रति व्यक्ति वोट सीमित करें", - "Choose between 2–10 options": "2–10 विकल्प चुनें", + "Mute Group": "ग्रुप म्यूट करें", "Mute User": "उपयोगकर्ता को म्यूट करें", + "Network error": "नेटवर्क त्रुटि", "No chats here yet…": "अभी तक यहाँ कोई चैट नहीं है...", + "No conversations yet": "अभी तक कोई चैट नहीं है", "No items exist": "कोई आइटम मौजूद नहीं", + "No messages yet": "अभी तक कोई मैसेज नहीं है", "No threads here yet": "यहाँ अभी तक कोई थ्रेड्स नहीं हैं", "Not supported": "समर्थित नहीं", "Nothing yet...": "कोई मैसेज नहीं है...", + "Offline": "ऑफलाइन", "Ok": "ठीक", + "Online": "ऑनलाइन", "Only visible to you": "केवल आपको दिखाई दे रहा है", + "Open Camera": "कैमरा खोलें", + "Open Files": "फ़ाइलें खोलें", "Open Settings": "सेटिंग्स खोलें", "Option": "विकल्प", - "Option {{count}}": "विकल्प {{count}}", "Option already exists": "विकल्प पहले से मौजूद है", + "Option {{count}}": "विकल्प {{count}}", "Options": "विकल्प", "Photo": "तस्वीर", "Photos and Videos": "तस्वीरें और वीडियों", @@ -97,16 +122,23 @@ "Poll Comments": "सर्वेक्षण टिप्पणियाँ", "Poll Options": "सर्वेक्षण विकल्प", "Poll Results": "सर्वेक्षण परिणाम", + "Poll has ended": "वोट समाप्त", "Questions": "प्रश्न", "Reconnecting...": "पुनः कनेक्ट हो...", + "Reminder overdue": "रीमिंडर ओवरडो", + "Reminder set": "रीमिंडर सेट किया गया", + "Replied to a thread": "थ्रेड में उत्तर दिया", "Reply": "मैसेज को रिप्लाई करे", - "Reply to {{name}}": "{{name}} को जवाब दें", "Reply to Message": "संदेश का जवाब दें", + "Reply to a message to start a thread": "एक संदेश का जवाब देकर थ्रेड शुरू करें", + "Reply to {{name}}": "{{name}} को जवाब दें", "Resend": "पुन: भेजें", "Retry Upload": "अपलोड पुनः प्रयास करें", "SEND": "भेजें", "Search": "खोजें", "Select More Photos": "अधिक फ़ोटो चुनें", + "Select files to share": "साझा करने के लिए फ़ाइलें चुनें", + "Select more than one option": "एक से अधिक विकल्प चुनें", "Select one": "एक चुनें", "Select one or more": "एक या अधिक चुनें", "Select up to {{count}}_many": "{{count}} तक चुनें", @@ -120,29 +152,61 @@ "Slow mode ON": "स्लो मोड चालू", "Slow mode, wait {{seconds}}s...": "स्लो मोड, {{seconds}}s इंतजार करें...", "Suggest an option": "एक विकल्प सुझाएं", + "Take a photo and share": "एक फ़ोटो लें और साझा करें", + "Take a video and share": "वीडियो लें और साझा करें", + "Tap to remove": "हटाने के लिए टैप करें", "The message has been reported to a moderator.": "संदेश एक मॉडरेटर को सूचित किया गया है।", "The source message was deleted": "स्रोत संदेश हटा दिया गया है", "Thinking...": "सोच रहा है...", "This reply was deleted": "यह उत्तर हटा दिया गया है", "Thread Reply": "धागा जवाब", "Type a number from 2 to 10": "2 से 10 के बीच एक संख्या दर्ज करें", + "Typing": "लिख रहा है", + "Unarchive Chat": "चैट को संग्रह से निकालें", + "Unarchive Group": "ग्रुप को संग्रह से निकालें", "Unban User": "उपयोगकर्ता को अनब्लॉक करें", "Unblock User": "उपयोगकर्ता को अनब्लॉक करें", "Unknown User": "अज्ञात उपयोगकर्ता", + "Unmute Group": "ग्रुप अनम्यूट करें", "Unmute User": "उपयोगकर्ता को अनम्यूट करें", "Unpin from Conversation": "बातचीत से अनपिन करें", "Unread Messages": "अपठित संदेश", + "Unsupported Attachment": "असमर्थित अटैचमेंट", "Update your comment": "अपनी टिप्पणी अपडेट करें", "Video": "वीडियो", + "View": "देखें", "View Results": "परिणाम देखें", "View {{count}} comments_many": "सभी {{count}} टिप्पणियाँ देखें", "View {{count}} comments_one": "{{count}} टिप्पणी देखें", "View {{count}} comments_other": "{{count}} टिप्पणियाँ देखें", "Voice message": "वॉइस संदेश", "Voice message ({{duration}})": "वॉइस संदेश ({{duration}})", - "Your comment": "आपकी टिप्पणी", "You": "आप", "You can't send messages in this channel": "आप इस चैनल में संदेश नहीं भेज सकते", + "You have not granted access to the photo library.": "आपने फोटो लाइब्रेरी के लिए अनुमति नहीं दी है।", + "You have not granted access to your camera": "आपने अपने कैमरे के लिए अनुमति नहीं दी है", + "You voted: {{ option }}": "आपने वोट दिया: {{ option }}", + "Your comment": "आपकी टिप्पणी", + "and {{ count }} others": "और {{ count }} अन्य", + "aria/AI is generating": "AI is generating", + "aria/AI is thinking": "AI is thinking", + "aria/Avatar of {{name}}": "Avatar of {{name}}", + "aria/Connected": "Connected", + "aria/Delivered": "Delivered", + "aria/Loading": "Loading", + "aria/Loading failed": "Loading failed", + "aria/New message from {{user}}": "New message from {{user}}", + "aria/Offline": "Offline", + "aria/Open message actions": "Open message actions", + "aria/Reaction {{emoji}} by {{count}} users": "Reaction {{emoji}} by {{count}} users", + "aria/Read": "Read", + "aria/Reconnecting": "Reconnecting", + "aria/Reply to {{user}}": "Reply to {{user}}", + "aria/Send message": "Send message", + "aria/Sending": "Sending", + "aria/Sent": "Sent", + "aria/Voice message recording. Hold to record.": "Voice message recording. Hold to record.", + "aria/{{count}} new messages": "{{count}} new messages", "duration/Location end at": "{{ milliseconds | durationFormatter(withSuffix: false) }}", "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", @@ -158,16 +222,11 @@ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[कल]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[कल]\",\"nextWeek\":\"dddd [को] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} और {{ nonSelfUserLength }} अधिक टाइप कर रहे हैं", + "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} और {{ secondUser }} लिख रहे हैं", "{{ index }} of {{ photoLength }}": "{{ index }} / {{ photoLength }}", + "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} लोग लिख रहे हैं", "{{ replyCount }} Replies": "{{ replyCount }} रिप्लाई", "{{ user }} is typing": "{{ user }} टाइप कर रहा है", - "You voted: {{ option }}": "आपने वोट दिया: {{ option }}", - "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} और {{ secondUser }} लिख रहे हैं", - "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} लोग लिख रहे हैं", - "Typing": "लिख रहा है", - "No messages yet": "अभी तक कोई मैसेज नहीं है", - "Message failed to send": "मैसेज भेजने में विफल", - "and {{ count }} others": "और {{ count }} अन्य", "{{ user }} voted: {{ option }}": "{{ user }} वोट दिया: {{ option }}", "{{count}} Audios_many": "{{count}} ऑडियो", "{{count}} Audios_one": "{{count}} ऑडियो", @@ -178,63 +237,23 @@ "{{count}} Photos_many": "{{count}} फ़ोटो", "{{count}} Photos_one": "{{count}} फ़ोटो", "{{count}} Photos_other": "{{count}} फ़ोटो", - "{{count}} Voice messages_many": "{{count}} वॉइस संदेश", - "{{count}} Voice messages_one": "{{count}} वॉइस संदेश", - "{{count}} Voice messages_other": "{{count}} वॉइस संदेश", + "{{count}} Reactions_many": "{{count}} प्रतिक्रियाएँ", + "{{count}} Reactions_one": "{{count}} प्रतिक्रिया", + "{{count}} Reactions_other": "{{count}} प्रतिक्रियाएँ", "{{count}} Videos_many": "{{count}} वीडियो", "{{count}} Videos_one": "{{count}} वीडियो", "{{count}} Videos_other": "{{count}} वीडियो", + "{{count}} Voice messages_many": "{{count}} वॉइस संदेश", + "{{count}} Voice messages_one": "{{count}} वॉइस संदेश", + "{{count}} Voice messages_other": "{{count}} वॉइस संदेश", + "{{count}} new messages": "{{count}} नये संदेश", + "{{count}} new threads": "{{count}} नये थ्रेड्स", + "{{count}} unread": "{{count}} ना पढ़े हुए", "{{count}} votes_many": "{{count}} वोट", "{{count}} votes_one": "{{count}} वोट", "{{count}} votes_other": "{{count}} वोट", - "🏙 Attachment...": "🏙 अटैचमेंट...", - "You have not granted access to the photo library.": "आपने फोटो लाइब्रेरी के लिए अनुमति नहीं दी है।", - "Change in Settings": "सेटिंग्स में बदलें", - "Create a poll and let everyone vote": "पोल बनाएँ और सभी को वोट करने दें", - "Open Camera": "कैमरा खोलें", - "Open Files": "फ़ाइलें खोलें", - "Select files to share": "साझा करने के लिए फ़ाइलें चुनें", - "Take a photo and share": "एक फ़ोटो लें और साझा करें", - "Take a video and share": "वीडियो लें और साझा करें", - "You have not granted access to your camera": "आपने अपने कैमरे के लिए अनुमति नहीं दी है", - "{{count}} Reactions_many": "{{count}} प्रतिक्रियाएँ", - "{{count}} Reactions_one": "{{count}} प्रतिक्रिया", - "{{count}} Reactions_other": "{{count}} प्रतिक्रियाएँ", - "Tap to remove": "हटाने के लिए टैप करें", - "Draft": "ड्राफ्ट", - "Reminder set": "रीमिंडर सेट किया गया", - "Also sent in channel": "चैनल में भी भेजा गया", - "Replied to a thread": "थ्रेड में उत्तर दिया", - "View": "देखें", - "Reminder overdue": "रीमिंडर ओवरडो", - "Poll has ended": "वोट समाप्त", - "Reply to a message to start a thread": "एक संदेश का जवाब देकर थ्रेड शुरू करें", - "Couldn't load new threads. Tap to retry": "नये थ्रेड्स लोड नहीं हो सके। टैप करके पुनः कोशिश करें", - "{{count}} new threads": "{{count}} नये थ्रेड्स", - "No conversations yet": "अभी तक कोई चैट नहीं है", - "Are you sure you want to delete this group? This can't be undone.": "क्या आप वाकई इस ग्रुप को हटाना चाहते हैं? यह वापस नहीं किया जा सकता।", - "Are you sure you want to delete this chat? This can't be undone.": "क्या आप वाकई इस चैट को हटाना चाहते हैं? यह वापस नहीं किया जा सकता।", - "Delete chat": "चैट हटाएं", - "Delete group": "ग्रुप हटाएं", - "Archive Chat": "चैट संग्रहित करें", - "Archive Group": "ग्रुप संग्रहित करें", - "Delete Chat": "चैट हटाएं", - "Delete Group": "ग्रुप हटाएं", - "Leave Chat": "चैट छोड़ें", - "Leave Group": "ग्रुप छोड़ें", - "Mute Group": "ग्रुप म्यूट करें", - "Offline": "ऑफलाइन", - "Online": "ऑनलाइन", - "Unarchive Chat": "चैट को संग्रह से निकालें", - "Unarchive Group": "ग्रुप को संग्रह से निकालें", - "Unmute Group": "ग्रुप अनम्यूट करें", + "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} सदस्य, {{onlineCount}} ऑनलाइन", "{{memberCount}} members, {{onlineCount}} online_one": "{{memberCount}} सदस्य, {{onlineCount}} ऑनलाइन", "{{memberCount}} members, {{onlineCount}} online_other": "{{memberCount}} सदस्य, {{onlineCount}} ऑनलाइन", - "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} सदस्य, {{onlineCount}} ऑनलाइन", - "{{count}} unread": "{{count}} ना पढ़े हुए", - "{{count}} new messages": "{{count}} नये संदेश", - "Unsupported Attachment": "असमर्थित अटैचमेंट", - "+{{count}} More Options_one": "+{{count}} और विकल्प", - "+{{count}} More Options_other": "+{{count}} और विकल्प", - "+{{count}} More Options_many": "+{{count}} और विकल्प" + "🏙 Attachment...": "🏙 अटैचमेंट..." } diff --git a/package/src/i18n/it.json b/package/src/i18n/it.json index ebf1fc3bfd..5ae51dc03f 100644 --- a/package/src/i18n/it.json +++ b/package/src/i18n/it.json @@ -1,4 +1,7 @@ { + "+{{count}} More Options_many": "+{{count}} altre opzioni", + "+{{count}} More Options_one": "+{{count}} altra opzione", + "+{{count}} More Options_other": "+{{count}} altre opzioni", "+{{count}}_many": "+{{count}}", "+{{count}}_one": "+{{count}}", "+{{count}}_other": "+{{count}}", @@ -11,8 +14,13 @@ "Allow access to your Gallery": "Consenti l'accesso alla tua galleria", "Allow camera access in device settings": "Consenti l'accesso alla fotocamera nelle impostazioni del dispositivo", "Also send to channel": "Invia anche al canale", + "Also sent in channel": "También enviado en el canal", "Anonymous": "Anonimo", "Anonymous voting": "Sondaggio anonimo", + "Archive Chat": "Archivia chat", + "Archive Group": "Archivia gruppo", + "Are you sure you want to delete this chat? This can't be undone.": "Sei sicuro di voler eliminare questa chat? Questa azione non può essere annullata.", + "Are you sure you want to delete this group? This can't be undone.": "Sei sicuro di voler eliminare questo gruppo? Questa azione non può essere annullata.", "Are you sure you want to permanently delete this message?": "Sei sicuro di voler eliminare definitivamente questo messaggio?", "Are you sure?": "Sei sicuro?", "Ask a question": "Fai una domanda", @@ -21,23 +29,32 @@ "Block User": "Blocca Utente", "Cancel": "Annulla", "Cannot Flag Message": "Impossibile Segnalare Messaggio", + "Change in Settings": "Cambia in Impostazioni", + "Choose between 2–10 options": "Scegli tra 2 e 10 opzioni", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Considera come il tuo commento potrebbe far sentire gli altri e assicurati di seguire le nostre Linee guida della community", "Copy Message": "Copia Messaggio", + "Couldn't load new threads. Tap to retry": "Impossibile caricare nuovi thread. Tocca per riprovare", "Create Poll": "Crea sondaggio", + "Create a poll and let everyone vote": "Crea un sondaggio e lascia che tutti votino", "Delete": "Elimina", + "Delete Chat": "Elimina chat", + "Delete Group": "Elimina gruppo", "Delete Message": "Cancella il Messaggio", + "Delete chat": "Elimina chat", "Delete for me": "Elimina per me", + "Delete group": "Elimina gruppo", "Device camera is used to take photos or videos.": "La fotocamera del dispositivo viene utilizzata per scattare foto o video.", "Device gallery permissions is used to take photos or videos.": "Le autorizzazioni della galleria del dispositivo vengono utilizzate per scattare foto o video.", "Do you want to send a copy of this message to a moderator for further investigation?": "Vuoi inviare una copia di questo messaggio a un moderatore per ulteriori indagini?", + "Draft": "Borrador", "Due since {{ dueSince }}": "Scadenza dal {{ dueSince }}", "Edit Message": "Modifica Messaggio", "Edited": "Modificato", "Editing Message": "Modificando il Messaggio", "Emoji matching": "Abbinamento emoji", "Empty message...": "Message vuoto...", - "Enter a new option": "Inserisci una nuova opzione", "End Vote": "Termina votazione", + "Enter a new option": "Inserisci una nuova opzione", "Error loading": "Errore di caricamento", "Error loading channel list...": "Errore durante il caricamento della lista dei canali...", "Error loading messages for this channel...": "Errore durante il caricamento dei messaggi per questo canale...", @@ -55,8 +72,11 @@ "Hold to start recording.": "Tieni premuto per avviare la registrazione.", "How about sending your first message to a friend?": "Che ne dici di inviare il tuo primo messaggio ad un amico?", "Instant Commands": "Comandi Istantanei", + "Leave Chat": "Lascia chat", + "Leave Group": "Lascia gruppo", "Let others add options": "Permetti ad altri di aggiungere opzioni", "Let's start chatting!": "Iniziamo a chattare!", + "Limit votes per person": "Limita i voti per persona", "Links are disabled": "I link sono disabilitati", "Live Location": "Posizione in tempo reale", "Loading channels...": "Caricamento canali in corso...", @@ -68,24 +88,29 @@ "Maximum number of files reached": "Numero massimo di file raggiunto", "Message Reactions": "Reazioni ai Messaggi", "Message deleted": "Messaggio cancellato", + "Message failed to send": "Il messaggio non è stato inviato", "Message flagged": "Messaggio contrassegnato", "Multiple votes": "Voti multipli", - "Network error": "Errore di rete", - "Select more than one option": "Seleziona più di un'opzione", - "Limit votes per person": "Limita i voti per persona", - "Choose between 2–10 options": "Scegli tra 2 e 10 opzioni", + "Mute Group": "Disattiva audio gruppo", "Mute User": "Utente Muto", + "Network error": "Errore di rete", "No chats here yet…": "Non ci sono ancora chat qui...", + "No conversations yet": "Ancora nessuna conversazione", "No items exist": "Nessun elemento", + "No messages yet": "Ancora nessun messaggio", "No threads here yet": "Nessun thread qui ancora", "Not supported": "non supportato", "Nothing yet...": "Ancora niente...", + "Offline": "Offline", "Ok": "Ok", + "Online": "Online", "Only visible to you": "Visibile solo a te", + "Open Camera": "Apri fotocamera", + "Open Files": "Apri file", "Open Settings": "Apri Impostazioni", "Option": "Opzione", - "Option {{count}}": "Opzione {{count}}", "Option already exists": "L'opzione esiste già", + "Option {{count}}": "Opzione {{count}}", "Options": "Opzioni", "Photo": "Foto", "Photos and Videos": "Foto e Video", @@ -97,16 +122,23 @@ "Poll Comments": "Commenti sul sondaggio", "Poll Options": "Opzioni del sondaggio", "Poll Results": "Risultati del sondaggio", + "Poll has ended": "Votazione terminata", "Questions": "Domande", "Reconnecting...": "Ricollegarsi...", + "Reminder overdue": "Recordatorio vencido", + "Reminder set": "Recordatorio establecido", + "Replied to a thread": "Respondido a un hilo", "Reply": "Rispondi", - "Reply to {{name}}": "Rispondi a {{name}}", "Reply to Message": "Rispondi al messaggio", + "Reply to a message to start a thread": "Rispondi a un messaggio per iniziare un thread", + "Reply to {{name}}": "Rispondi a {{name}}", "Resend": "Invia di nuovo", "Retry Upload": "Riprova caricamento", "SEND": "INVIA", "Search": "Cerca", "Select More Photos": "Seleziona Altre foto", + "Select files to share": "Seleziona file da condividere", + "Select more than one option": "Seleziona più di un'opzione", "Select one": "Seleziona una", "Select one or more": "Seleziona una o più", "Select up to {{count}}_many": "Seleziona fino a {{count}}", @@ -120,29 +152,61 @@ "Slow mode ON": "Slowmode attiva", "Slow mode, wait {{seconds}}s...": "Slowmode, attendi {{seconds}}s...", "Suggest an option": "Suggerisci un'opzione", + "Take a photo and share": "Scatta una foto e condividila", + "Take a video and share": "Registra un video e condividilo", + "Tap to remove": "Tocca per rimuovere", "The message has been reported to a moderator.": "Il messaggio è stato segnalato a un moderatore.", "The source message was deleted": "Il messaggio originale è stato eliminato", "Thinking...": "Pensando...", "This reply was deleted": "Questa risposta è stata eliminata", "Thread Reply": "Rispondi alla Discussione", "Type a number from 2 to 10": "Digita un numero da 2 a 10", + "Typing": "Scrivendo", + "Unarchive Chat": "Rimuovi chat dall'archivio", + "Unarchive Group": "Rimuovi gruppo dall'archivio", "Unban User": "Sblocca Utente", "Unblock User": "Sblocca utente", "Unknown User": "Utente sconosciuto", + "Unmute Group": "Riattiva audio gruppo", "Unmute User": "Riattiva utente", "Unpin from Conversation": "Rimuovi dagli elementi in evidenza", "Unread Messages": "Messaggi non letti", + "Unsupported Attachment": "Allegato non supportato", "Update your comment": "Aggiorna il tuo commento", "Video": "Video", + "View": "Ver", "View Results": "Visualizza i risultati", "View {{count}} comments_many": "Vedi {{count}} commenti", "View {{count}} comments_one": "Vedi {{count}} commento", "View {{count}} comments_other": "Vedi {{count}} commenti", "Voice message": "Messaggio vocale", "Voice message ({{duration}})": "Messaggio vocale ({{duration}})", - "Your comment": "Il tuo commento", "You": "Tu", "You can't send messages in this channel": "Non puoi inviare messaggi in questo canale", + "You have not granted access to the photo library.": "Non hai concesso l'accesso alla libreria foto.", + "You have not granted access to your camera": "Non hai concesso l’accesso alla fotocamera", + "You voted: {{ option }}": "Hai votato: {{ option }}", + "Your comment": "Il tuo commento", + "and {{ count }} others": "e {{ count }} altri", + "aria/AI is generating": "AI is generating", + "aria/AI is thinking": "AI is thinking", + "aria/Avatar of {{name}}": "Avatar of {{name}}", + "aria/Connected": "Connected", + "aria/Delivered": "Delivered", + "aria/Loading": "Loading", + "aria/Loading failed": "Loading failed", + "aria/New message from {{user}}": "New message from {{user}}", + "aria/Offline": "Offline", + "aria/Open message actions": "Open message actions", + "aria/Reaction {{emoji}} by {{count}} users": "Reaction {{emoji}} by {{count}} users", + "aria/Read": "Read", + "aria/Reconnecting": "Reconnecting", + "aria/Reply to {{user}}": "Reply to {{user}}", + "aria/Send message": "Send message", + "aria/Sending": "Sending", + "aria/Sent": "Sent", + "aria/Voice message recording. Hold to record.": "Voice message recording. Hold to record.", + "aria/{{count}} new messages": "{{count}} new messages", "duration/Location end at": "{{ milliseconds | durationFormatter(withSuffix: false) }}", "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", @@ -158,16 +222,11 @@ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Ieri]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Domani]\",\"nextWeek\":\"dddd [alle] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} e altri {{ nonSelfUserLength }} stanno scrivendo", + "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} e {{ secondUser }} stanno scrivendo", "{{ index }} of {{ photoLength }}": "{{ index }} di {{ photoLength }}", + "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} persone stanno scrivendo", "{{ replyCount }} Replies": "{{ replyCount }} Risposte", "{{ user }} is typing": "{{ user }} sta scrivendo", - "You voted: {{ option }}": "Hai votato: {{ option }}", - "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} e {{ secondUser }} stanno scrivendo", - "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} persone stanno scrivendo", - "Typing": "Scrivendo", - "No messages yet": "Ancora nessun messaggio", - "Message failed to send": "Il messaggio non è stato inviato", - "and {{ count }} others": "e {{ count }} altri", "{{ user }} voted: {{ option }}": "{{ user }} ha votato: {{ option }}", "{{count}} Audios_many": "{{count}} audio", "{{count}} Audios_one": "{{count}} audio", @@ -178,63 +237,23 @@ "{{count}} Photos_many": "{{count}} foto", "{{count}} Photos_one": "{{count}} foto", "{{count}} Photos_other": "{{count}} foto", - "{{count}} Voice messages_many": "{{count}} messaggi vocali", - "{{count}} Voice messages_one": "{{count}} messaggio vocale", - "{{count}} Voice messages_other": "{{count}} messaggi vocali", + "{{count}} Reactions_many": "{{count}} reazioni", + "{{count}} Reactions_one": "{{count}} reazione", + "{{count}} Reactions_other": "{{count}} reazioni", "{{count}} Videos_many": "{{count}} video", "{{count}} Videos_one": "{{count}} video", "{{count}} Videos_other": "{{count}} video", + "{{count}} Voice messages_many": "{{count}} messaggi vocali", + "{{count}} Voice messages_one": "{{count}} messaggio vocale", + "{{count}} Voice messages_other": "{{count}} messaggi vocali", + "{{count}} new messages": "{{count}} nuovi messaggi", + "{{count}} new threads": "{{count}} nuovi thread", + "{{count}} unread": "{{count}} non letti", "{{count}} votes_many": "{{count}} voti", "{{count}} votes_one": "{{count}} voto", "{{count}} votes_other": "{{count}} voti", - "🏙 Attachment...": "🏙 Allegato...", - "You have not granted access to the photo library.": "Non hai concesso l'accesso alla libreria foto.", - "Change in Settings": "Cambia in Impostazioni", - "Create a poll and let everyone vote": "Crea un sondaggio e lascia che tutti votino", - "Open Camera": "Apri fotocamera", - "Open Files": "Apri file", - "Select files to share": "Seleziona file da condividere", - "Take a photo and share": "Scatta una foto e condividila", - "Take a video and share": "Registra un video e condividilo", - "You have not granted access to your camera": "Non hai concesso l’accesso alla fotocamera", - "{{count}} Reactions_many": "{{count}} reazioni", - "{{count}} Reactions_one": "{{count}} reazione", - "{{count}} Reactions_other": "{{count}} reazioni", - "Tap to remove": "Tocca per rimuovere", - "Draft": "Borrador", - "Reminder set": "Recordatorio establecido", - "Also sent in channel": "También enviado en el canal", - "Replied to a thread": "Respondido a un hilo", - "View": "Ver", - "Reminder overdue": "Recordatorio vencido", - "Poll has ended": "Votazione terminata", - "Reply to a message to start a thread": "Rispondi a un messaggio per iniziare un thread", - "Couldn't load new threads. Tap to retry": "Impossibile caricare nuovi thread. Tocca per riprovare", - "{{count}} new threads": "{{count}} nuovi thread", - "No conversations yet": "Ancora nessuna conversazione", - "Are you sure you want to delete this group? This can't be undone.": "Sei sicuro di voler eliminare questo gruppo? Questa azione non può essere annullata.", - "Are you sure you want to delete this chat? This can't be undone.": "Sei sicuro di voler eliminare questa chat? Questa azione non può essere annullata.", - "Delete chat": "Elimina chat", - "Delete group": "Elimina gruppo", - "Archive Chat": "Archivia chat", - "Archive Group": "Archivia gruppo", - "Delete Chat": "Elimina chat", - "Delete Group": "Elimina gruppo", - "Leave Chat": "Lascia chat", - "Leave Group": "Lascia gruppo", - "Mute Group": "Disattiva audio gruppo", - "Offline": "Offline", - "Online": "Online", - "Unarchive Chat": "Rimuovi chat dall'archivio", - "Unarchive Group": "Rimuovi gruppo dall'archivio", - "Unmute Group": "Riattiva audio gruppo", + "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} membri, {{onlineCount}} online", "{{memberCount}} members, {{onlineCount}} online_one": "{{memberCount}} membro, {{onlineCount}} online", "{{memberCount}} members, {{onlineCount}} online_other": "{{memberCount}} membri, {{onlineCount}} online", - "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} membri, {{onlineCount}} online", - "{{count}} unread": "{{count}} non letti", - "{{count}} new messages": "{{count}} nuovi messaggi", - "Unsupported Attachment": "Allegato non supportato", - "+{{count}} More Options_one": "+{{count}} altra opzione", - "+{{count}} More Options_other": "+{{count}} altre opzioni", - "+{{count}} More Options_many": "+{{count}} altre opzioni" + "🏙 Attachment...": "🏙 Allegato..." } diff --git a/package/src/i18n/ja.json b/package/src/i18n/ja.json index a624563c91..16b74bea5b 100644 --- a/package/src/i18n/ja.json +++ b/package/src/i18n/ja.json @@ -1,4 +1,7 @@ { + "+{{count}} More Options_many": "+{{count}} 件の追加オプション", + "+{{count}} More Options_one": "+{{count}} 件の追加オプション", + "+{{count}} More Options_other": "+{{count}} 件の追加オプション", "+{{count}}_many": "+{{count}}", "+{{count}}_one": "+{{count}}", "+{{count}}_other": "+{{count}}", @@ -11,8 +14,13 @@ "Allow access to your Gallery": "ギャラリーへのアクセスを許可する", "Allow camera access in device settings": "デバイス設定でカメラへのアクセスを許可する", "Also send to channel": "チャンネルにも送信", + "Also sent in channel": "チャンネルにも送信", "Anonymous": "匿名", "Anonymous voting": "匿名アンケート", + "Archive Chat": "チャットをアーカイブ", + "Archive Group": "グループをアーカイブ", + "Are you sure you want to delete this chat? This can't be undone.": "このチャットを削除しますか?この操作は元に戻せません。", + "Are you sure you want to delete this group? This can't be undone.": "このグループを削除しますか?この操作は元に戻せません。", "Are you sure you want to permanently delete this message?": "このメッセージを完全に削除してもよろしいですか?", "Are you sure?": "本当によろしいですか?", "Ask a question": "質問をする", @@ -21,23 +29,32 @@ "Block User": "ユーザをブロックする", "Cancel": "キャンセル", "Cannot Flag Message": "メッセージをフラグできません", + "Change in Settings": "設定で変更", + "Choose between 2–10 options": "2~10個のオプションから選択", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "あなたのコメントが他の人にどのように影響するか考え、必ずコミュニティガイドラインに従ってください", "Copy Message": "メッセージのコピー", + "Couldn't load new threads. Tap to retry": "新しいスレッドを読み込めませんでした。タップして再試行", "Create Poll": "アンケートを作成", + "Create a poll and let everyone vote": "投票を作成してみんなに投票してもらう", "Delete": "消去", + "Delete Chat": "チャットを削除", + "Delete Group": "グループを削除", "Delete Message": "メッセージを削除", + "Delete chat": "チャットを削除", "Delete for me": "自分で削除", + "Delete group": "グループを削除", "Device camera is used to take photos or videos.": "デバイスのカメラは写真やビデオの撮影に使用されます。", "Device gallery permissions is used to take photos or videos.": "デバイスギャラリーの権限は写真やビデオを撮るために使用されます。", "Do you want to send a copy of this message to a moderator for further investigation?": "このメッセージのコピーをモデレーターに送信して、さらに調査しますか?", + "Draft": "下書き", "Due since {{ dueSince }}": "期限は {{ dueSince }} からです", "Edit Message": "メッセージを編集", "Edited": "編集済み", "Editing Message": "メッセージを編集中", "Emoji matching": "絵文字マッチング", "Empty message...": "空のメッセージ...", - "Enter a new option": "新しい選択肢を入力", "End Vote": "投票を終了", + "Enter a new option": "新しい選択肢を入力", "Error loading": "読み込みエラー", "Error loading channel list...": "チャネルリストの読み込み中にエラーが発生しました。。。", "Error loading messages for this channel...": "このチャネルのメッセージの読み込み中にエラーが発生しました。。。", @@ -55,8 +72,11 @@ "Hold to start recording.": "録音を開始するには押し続けてください。", "How about sending your first message to a friend?": "初めてのメッセージを友達に送ってみてはいかがでしょうか?", "Instant Commands": "インスタントコマンド", + "Leave Chat": "チャットを退出", + "Leave Group": "グループを退出", "Let others add options": "他の人が選択肢を追加できるようにする", "Let's start chatting!": "チャットを始めましょう!", + "Limit votes per person": "1人あたりの投票数を制限", "Links are disabled": "リンク機能が無効になっています", "Live Location": "ライブ位置情報", "Loading channels...": "チャネルを読み込み中。。。", @@ -68,24 +88,29 @@ "Maximum number of files reached": "ファイルの最大数に達しました", "Message Reactions": "メッセージのリアクション", "Message deleted": "メッセージが削除されました", + "Message failed to send": "メッセージを送信できませんでした", "Message flagged": "メッセージにフラグが付けられました", "Multiple votes": "複数投票", - "Network error": "ネットワークエラー", - "Select more than one option": "2つ以上のオプションを選択", - "Limit votes per person": "1人あたりの投票数を制限", - "Choose between 2–10 options": "2~10個のオプションから選択", + "Mute Group": "グループをミュート", "Mute User": "ユーザーをミュートする", + "Network error": "ネットワークエラー", "No chats here yet…": "まだチャットはありません…", + "No conversations yet": "まだ会話がありません", "No items exist": "項目がありません", + "No messages yet": "まだメッセージがありません", "No threads here yet": "まだスレッドがありません", "Not supported": "サポートしていません", "Nothing yet...": "まだ何もありません...", + "Offline": "オフライン", "Ok": "確認", + "Online": "オンライン", "Only visible to you": "あなただけに見える", + "Open Camera": "カメラを開く", + "Open Files": "ファイルを開く", "Open Settings": "設定を開く", "Option": "オプション", - "Option {{count}}": "オプション {{count}}", "Option already exists": "オプションはすでに存在します", + "Option {{count}}": "オプション {{count}}", "Options": "オプション", "Photo": "写真", "Photos and Videos": "写真と動画", @@ -97,16 +122,23 @@ "Poll Comments": "アンケートのコメント", "Poll Options": "アンケートのオプション", "Poll Results": "アンケートの結果", + "Poll has ended": "投票終了", "Questions": "質問", "Reconnecting...": "再接続中。。。", + "Reminder overdue": "リマインダー期限切れ", + "Reminder set": "リマインダー設定", + "Replied to a thread": "スレッドに返信", "Reply": "返事", - "Reply to {{name}}": "{{name}}に返信", "Reply to Message": "メッセージに返信", + "Reply to a message to start a thread": "メッセージに返信してスレッドを開始", + "Reply to {{name}}": "{{name}}に返信", "Resend": "再送", "Retry Upload": "アップロードを再試行", "SEND": "送信", "Search": "検索", "Select More Photos": "さらに写真を選択", + "Select files to share": "共有するファイルを選択", + "Select more than one option": "2つ以上のオプションを選択", "Select one": "1つ選択", "Select one or more": "1つ以上選択", "Select up to {{count}}_many": "{{count}} まで選択", @@ -120,29 +152,61 @@ "Slow mode ON": "スローモードオン", "Slow mode, wait {{seconds}}s...": "スローモード, {{seconds}}s 待ってください...", "Suggest an option": "オプションを提案", + "Take a photo and share": "写真を撮って共有", + "Take a video and share": "動画を撮影して共有", + "Tap to remove": "タップして削除", "The message has been reported to a moderator.": "メッセージはモデレーターに報告されました。", "The source message was deleted": "元のメッセージが削除されました", "Thinking...": "考え中...", "This reply was deleted": "この返信は削除されました", "Thread Reply": "スレッドの返信", "Type a number from 2 to 10": "2から10の数字を入力してください", + "Typing": "タイピング中", + "Unarchive Chat": "チャットのアーカイブを解除", + "Unarchive Group": "グループのアーカイブを解除", "Unban User": "ユーザーの禁止を解除する", "Unblock User": "ユーザーのブロックを解除する", "Unknown User": "不明なユーザー", + "Unmute Group": "グループのミュートを解除", "Unmute User": "ユーザーのミュートを解除する", "Unpin from Conversation": "会話のピンを外す", "Unread Messages": "未読メッセージ", + "Unsupported Attachment": "サポートされていない添付ファイル", "Update your comment": "コメントを更新", "Video": "ビデオ", + "View": "表示", "View Results": "結果を表示", "View {{count}} comments_many": "すべての{{count}}コメントを表示", "View {{count}} comments_one": "{{count}} 件のコメントを表示", "View {{count}} comments_other": "{{count}} 件のコメントを表示", "Voice message": "ボイスメッセージ", "Voice message ({{duration}})": "ボイスメッセージ({{duration}})", - "Your comment": "あなたのコメント", "You": "あなた", "You can't send messages in this channel": "このチャンネルではメッセージを送信できません", + "You have not granted access to the photo library.": "写真ライブラリへのアクセスが許可されていません。", + "You have not granted access to your camera": "カメラへのアクセスが許可されていません", + "You voted: {{ option }}": "あなたが投票しました: {{ option }}", + "Your comment": "あなたのコメント", + "and {{ count }} others": "{{ count }}人がタイピングしています", + "aria/AI is generating": "AI is generating", + "aria/AI is thinking": "AI is thinking", + "aria/Avatar of {{name}}": "Avatar of {{name}}", + "aria/Connected": "Connected", + "aria/Delivered": "Delivered", + "aria/Loading": "Loading", + "aria/Loading failed": "Loading failed", + "aria/New message from {{user}}": "New message from {{user}}", + "aria/Offline": "Offline", + "aria/Open message actions": "Open message actions", + "aria/Reaction {{emoji}} by {{count}} users": "Reaction {{emoji}} by {{count}} users", + "aria/Read": "Read", + "aria/Reconnecting": "Reconnecting", + "aria/Reply to {{user}}": "Reply to {{user}}", + "aria/Send message": "Send message", + "aria/Sending": "Sending", + "aria/Sent": "Sent", + "aria/Voice message recording. Hold to record.": "Voice message recording. Hold to record.", + "aria/{{count}} new messages": "{{count}} new messages", "duration/Location end at": "{{ milliseconds | durationFormatter(withSuffix: false) }}", "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", @@ -158,16 +222,11 @@ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[昨日]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[明日]\",\"nextWeek\":\"dddd [の] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }}と{{ nonSelfUserLength }}人がタイピングしています", + "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }}と{{ secondUser }}がタイピングしています", "{{ index }} of {{ photoLength }}": "{{ index }} / {{ photoLength }}", + "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }}人がタイピングしています", "{{ replyCount }} Replies": "{{ replyCount }}件の返信", "{{ user }} is typing": "{{ user }}はタイピング中", - "You voted: {{ option }}": "あなたが投票しました: {{ option }}", - "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }}と{{ secondUser }}がタイピングしています", - "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }}人がタイピングしています", - "Typing": "タイピング中", - "No messages yet": "まだメッセージがありません", - "Message failed to send": "メッセージを送信できませんでした", - "and {{ count }} others": "{{ count }}人がタイピングしています", "{{ user }} voted: {{ option }}": "{{ user }} が投票しました: {{ option }}", "{{count}} Audios_many": "{{count}} 件の音声", "{{count}} Audios_one": "{{count}} 件の音声", @@ -178,63 +237,23 @@ "{{count}} Photos_many": "{{count}} 枚の写真", "{{count}} Photos_one": "{{count}} 枚の写真", "{{count}} Photos_other": "{{count}} 枚の写真", - "{{count}} Voice messages_many": "{{count}} 件のボイスメッセージ", - "{{count}} Voice messages_one": "{{count}} 件のボイスメッセージ", - "{{count}} Voice messages_other": "{{count}} 件のボイスメッセージ", + "{{count}} Reactions_many": "{{count}}件のリアクション", + "{{count}} Reactions_one": "{{count}}件のリアクション", + "{{count}} Reactions_other": "{{count}}件のリアクション", "{{count}} Videos_many": "{{count}} 件の動画", "{{count}} Videos_one": "{{count}} 件の動画", "{{count}} Videos_other": "{{count}} 件の動画", + "{{count}} Voice messages_many": "{{count}} 件のボイスメッセージ", + "{{count}} Voice messages_one": "{{count}} 件のボイスメッセージ", + "{{count}} Voice messages_other": "{{count}} 件のボイスメッセージ", + "{{count}} new messages": "{{count}} 新しいメッセージ", + "{{count}} new threads": "{{count}} 新しいスレッド", + "{{count}} unread": "{{count}} 未読", "{{count}} votes_many": "{{count}}票", "{{count}} votes_one": "{{count}} 票", "{{count}} votes_other": "{{count}} 票", - "🏙 Attachment...": "🏙 アタッチメント...", - "You have not granted access to the photo library.": "写真ライブラリへのアクセスが許可されていません。", - "Change in Settings": "設定で変更", - "Create a poll and let everyone vote": "投票を作成してみんなに投票してもらう", - "Open Camera": "カメラを開く", - "Open Files": "ファイルを開く", - "Select files to share": "共有するファイルを選択", - "Take a photo and share": "写真を撮って共有", - "Take a video and share": "動画を撮影して共有", - "You have not granted access to your camera": "カメラへのアクセスが許可されていません", - "{{count}} Reactions_many": "{{count}}件のリアクション", - "{{count}} Reactions_one": "{{count}}件のリアクション", - "{{count}} Reactions_other": "{{count}}件のリアクション", - "Tap to remove": "タップして削除", - "Draft": "下書き", - "Reminder set": "リマインダー設定", - "Also sent in channel": "チャンネルにも送信", - "Replied to a thread": "スレッドに返信", - "View": "表示", - "Reminder overdue": "リマインダー期限切れ", - "Poll has ended": "投票終了", - "Reply to a message to start a thread": "メッセージに返信してスレッドを開始", - "Couldn't load new threads. Tap to retry": "新しいスレッドを読み込めませんでした。タップして再試行", - "{{count}} new threads": "{{count}} 新しいスレッド", - "No conversations yet": "まだ会話がありません", - "Are you sure you want to delete this group? This can't be undone.": "このグループを削除しますか?この操作は元に戻せません。", - "Are you sure you want to delete this chat? This can't be undone.": "このチャットを削除しますか?この操作は元に戻せません。", - "Delete chat": "チャットを削除", - "Delete group": "グループを削除", - "Archive Chat": "チャットをアーカイブ", - "Archive Group": "グループをアーカイブ", - "Delete Chat": "チャットを削除", - "Delete Group": "グループを削除", - "Leave Chat": "チャットを退出", - "Leave Group": "グループを退出", - "Mute Group": "グループをミュート", - "Offline": "オフライン", - "Online": "オンライン", - "Unarchive Chat": "チャットのアーカイブを解除", - "Unarchive Group": "グループのアーカイブを解除", - "Unmute Group": "グループのミュートを解除", + "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}}人のメンバー、{{onlineCount}}人がオンライン", "{{memberCount}} members, {{onlineCount}} online_one": "{{memberCount}}人のメンバー、{{onlineCount}}人がオンライン", "{{memberCount}} members, {{onlineCount}} online_other": "{{memberCount}}人のメンバー、{{onlineCount}}人がオンライン", - "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}}人のメンバー、{{onlineCount}}人がオンライン", - "{{count}} unread": "{{count}} 未読", - "{{count}} new messages": "{{count}} 新しいメッセージ", - "Unsupported Attachment": "サポートされていない添付ファイル", - "+{{count}} More Options_one": "+{{count}} 件の追加オプション", - "+{{count}} More Options_other": "+{{count}} 件の追加オプション", - "+{{count}} More Options_many": "+{{count}} 件の追加オプション" + "🏙 Attachment...": "🏙 アタッチメント..." } diff --git a/package/src/i18n/ko.json b/package/src/i18n/ko.json index 38a4fccb64..9c06a054bc 100644 --- a/package/src/i18n/ko.json +++ b/package/src/i18n/ko.json @@ -1,4 +1,7 @@ { + "+{{count}} More Options_many": "+옵션 {{count}}개 더", + "+{{count}} More Options_one": "+옵션 {{count}}개 더", + "+{{count}} More Options_other": "+옵션 {{count}}개 더", "+{{count}}_many": "+{{count}}", "+{{count}}_one": "+{{count}}", "+{{count}}_other": "+{{count}}", @@ -11,8 +14,13 @@ "Allow access to your Gallery": "갤러리에 대한 액세스를 허용", "Allow camera access in device settings": "기기 설정에서 카메라 액세스를 허용하세요.", "Also send to channel": "채널에도 전송", + "Also sent in channel": "채널에도 전송", "Anonymous": "익명", "Anonymous voting": "익명 투표", + "Archive Chat": "채팅 보관", + "Archive Group": "그룹 보관", + "Are you sure you want to delete this chat? This can't be undone.": "이 채팅을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "Are you sure you want to delete this group? This can't be undone.": "이 그룹을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", "Are you sure you want to permanently delete this message?": "이 메시지를 영구적으로 삭제하시겠습니까?", "Are you sure?": "확실합니까?", "Ask a question": "질문하기", @@ -21,23 +29,32 @@ "Block User": "사용자 차단", "Cancel": "취소", "Cannot Flag Message": "메세지를 플래그 할 수 없습니다", + "Change in Settings": "설정에서 변경", + "Choose between 2–10 options": "2~10개의 옵션 중에서 선택하세요", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "당신의 댓글이 다른 사람들에게 어떤 영향을 줄지 고려하고 반드시 우리의 커뮤니티 가이드라인을 따르십시오", "Copy Message": "메시지 복사", + "Couldn't load new threads. Tap to retry": "새로운 스레드를 로드할 수 없습니다. 탭하여 다시 시도", "Create Poll": "투표 생성", + "Create a poll and let everyone vote": "투표를 만들어 모두가 투표하게 하세요", "Delete": "삭제", + "Delete Chat": "채팅 삭제", + "Delete Group": "그룹 삭제", "Delete Message": "메시지 삭제", + "Delete chat": "채팅 삭제", "Delete for me": "나 삭제", + "Delete group": "그룹 삭제", "Device camera is used to take photos or videos.": "기기 카메라는 사진이나 동영상을 촬영하는 데 사용됩니다.", "Device gallery permissions is used to take photos or videos.": "장치 갤러리 권한은 사진 또는 비디오를 촬영하는 데 사용됩니다.", "Do you want to send a copy of this message to a moderator for further investigation?": "이 메시지의 복사본을 운영자에게 보내 추가 조사를합니까?", + "Draft": "초안", "Due since {{ dueSince }}": "기한은 {{ dueSince }}부터입니다.", "Edit Message": "메시지 수정", "Edited": "편집됨", "Editing Message": "메시지 편집중", "Emoji matching": "이모티콘 매칭", "Empty message...": "빈 메시지...", - "Enter a new option": "새 옵션 입력", "End Vote": "투표 종료", + "Enter a new option": "새 옵션 입력", "Error loading": "로드 오류", "Error loading channel list...": "채널리스트 을로드하는 동안 오류가 발생했습니다...", "Error loading messages for this channel...": "이 채널의 메시지를로드하는 동안 오류가 발생했습니다...", @@ -55,8 +72,11 @@ "Hold to start recording.": "녹음을 시작하려면 눌러주세요.", "How about sending your first message to a friend?": "친구에게 첫 번째 메시지를 보내는 것은 어떻습니까?", "Instant Commands": "인스턴트 명령", + "Leave Chat": "채팅 나가기", + "Leave Group": "그룹 나가기", "Let others add options": "다른 사람이 옵션을 추가하도록 허용", "Let's start chatting!": "채팅을 시작합시다!", + "Limit votes per person": "1인당 투표 수 제한", "Links are disabled": "링크 기능이 비활성화되었습니다", "Live Location": "실시간 위치", "Loading channels...": "채널을 로딩 중...", @@ -68,24 +88,29 @@ "Maximum number of files reached": "최대 파일 수에 도달했습니다", "Message Reactions": "메시지의 리액션", "Message deleted": "메시지가 삭제되었습니다.", + "Message failed to send": "메시지 전송 실패", "Message flagged": "메시지에 플래그가 지정되었습니다", "Multiple votes": "복수 투표", - "Network error": "네트워크 오류", - "Select more than one option": "두 개 이상의 옵션을 선택하세요", - "Limit votes per person": "1인당 투표 수 제한", - "Choose between 2–10 options": "2~10개의 옵션 중에서 선택하세요", + "Mute Group": "그룹 음소거", "Mute User": "사용자를 음소거", + "Network error": "네트워크 오류", "No chats here yet…": "아직 여기에 채팅이 없어요…", + "No conversations yet": "아직 대화가 없습니다", "No items exist": "항목이 없습니다", + "No messages yet": "아직 메시지가 없습니다", "No threads here yet": "아직 스레드가 없습니다", "Not supported": "지원하지 않습니다", "Nothing yet...": "아직 아무것도...", + "Offline": "오프라인", "Ok": "확인", + "Online": "온라인", "Only visible to you": "당신만 볼 수 있습니다", + "Open Camera": "카메라 열기", + "Open Files": "파일 열기", "Open Settings": "설정 열기", "Option": "옵션", - "Option {{count}}": "옵션 {{count}}", "Option already exists": "옵션은 이미 존재합니다", + "Option {{count}}": "옵션 {{count}}", "Options": "옵션", "Photo": "사진", "Photos and Videos": "사진과 동영상", @@ -97,16 +122,23 @@ "Poll Comments": "투표 댓글", "Poll Options": "투표 옵션", "Poll Results": "투표 결과", + "Poll has ended": "투표 종료됨", "Questions": "질문", "Reconnecting...": "다시 연결 중...", + "Reminder overdue": "리마인더 만료", + "Reminder set": "리마인더 설정", + "Replied to a thread": "스레드에 답장", "Reply": "답장", - "Reply to {{name}}": "{{name}}님에게 답장", "Reply to Message": "메시지에 답장", + "Reply to a message to start a thread": "메시지에 답장하여 스레드 시작", + "Reply to {{name}}": "{{name}}님에게 답장", "Resend": "재전송", "Retry Upload": "업로드 재시도", "SEND": "보내기", "Search": "검색", "Select More Photos": "추가 사진 선택", + "Select files to share": "공유할 파일 선택", + "Select more than one option": "두 개 이상의 옵션을 선택하세요", "Select one": "하나 선택", "Select one or more": "하나 이상 선택", "Select up to {{count}}_many": "{{count}} 까지 선택", @@ -120,29 +152,61 @@ "Slow mode ON": "슬로모드 켜짐", "Slow mode, wait {{seconds}}s...": "슬로모드, {{seconds}}s 대기...", "Suggest an option": "옵션 제안", + "Take a photo and share": "사진을 찍고 공유", + "Take a video and share": "동영상을 촬영하고 공유", + "Tap to remove": "탭하여 제거", "The message has been reported to a moderator.": "메시지는 운영자에보고되었습니다.", "The source message was deleted": "원본 메시지가 삭제되었습니다", "Thinking...": "생각 중...", "This reply was deleted": "이 답글은 삭제되었습니다", "Thread Reply": "스레드 답장", "Type a number from 2 to 10": "2에서 10 사이의 숫자를 입력하세요", + "Typing": "타이핑 중", + "Unarchive Chat": "채팅 보관 해제", + "Unarchive Group": "그룹 보관 해제", "Unban User": "사용자 차단 해제", "Unblock User": "사용자 차단 해제", "Unknown User": "알 수없는 사용자", + "Unmute Group": "그룹 음소거 해제", "Unmute User": "사용자 음소거 해제", "Unpin from Conversation": "대화의 핀을 분리합니다", "Unread Messages": "읽지 않은 메시지", + "Unsupported Attachment": "지원하지 않는 첨부파일", "Update your comment": "댓글 수정", "Video": "동영상", + "View": "보기", "View Results": "결과 보기", "View {{count}} comments_many": "모든 {{count}} 댓글 보기", "View {{count}} comments_one": "{{count}}개의 댓글 보기", "View {{count}} comments_other": "{{count}}개의 댓글 보기", "Voice message": "음성 메시지", "Voice message ({{duration}})": "음성 메시지 ({{duration}})", - "Your comment": "댓글 입력", "You": "당신", "You can't send messages in this channel": "이 채널에서는 메세지를 전송할 수 없습니다", + "You have not granted access to the photo library.": "사진 라이브러리에 대한 접근 권한이 없습니다.", + "You have not granted access to your camera": "카메라 접근 권한이 없습니다", + "You voted: {{ option }}": "당신이 투표했습니다: {{ option }}", + "Your comment": "댓글 입력", + "and {{ count }} others": "{{ count }}명 이상", + "aria/AI is generating": "AI is generating", + "aria/AI is thinking": "AI is thinking", + "aria/Avatar of {{name}}": "Avatar of {{name}}", + "aria/Connected": "Connected", + "aria/Delivered": "Delivered", + "aria/Loading": "Loading", + "aria/Loading failed": "Loading failed", + "aria/New message from {{user}}": "New message from {{user}}", + "aria/Offline": "Offline", + "aria/Open message actions": "Open message actions", + "aria/Reaction {{emoji}} by {{count}} users": "Reaction {{emoji}} by {{count}} users", + "aria/Read": "Read", + "aria/Reconnecting": "Reconnecting", + "aria/Reply to {{user}}": "Reply to {{user}}", + "aria/Send message": "Send message", + "aria/Sending": "Sending", + "aria/Sent": "Sent", + "aria/Voice message recording. Hold to record.": "Voice message recording. Hold to record.", + "aria/{{count}} new messages": "{{count}} new messages", "duration/Location end at": "{{ milliseconds | durationFormatter(withSuffix: false) }}", "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", @@ -158,16 +222,11 @@ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[어제]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[내일]\",\"nextWeek\":\"dddd [LT에]\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} 외 {{ nonSelfUserLength }}명이 입력 중입니다", + "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }}와 {{ secondUser }}가 타이핑 중입니다", "{{ index }} of {{ photoLength }}": "{{ index }} / {{ photoLength }}", + "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }}명이 타이핑 중입니다", "{{ replyCount }} Replies": "{{ replyCount }} 답글", "{{ user }} is typing": "{{ user }} 타이핑 중", - "You voted: {{ option }}": "당신이 투표했습니다: {{ option }}", - "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }}와 {{ secondUser }}가 타이핑 중입니다", - "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }}명이 타이핑 중입니다", - "Typing": "타이핑 중", - "No messages yet": "아직 메시지가 없습니다", - "Message failed to send": "메시지 전송 실패", - "and {{ count }} others": "{{ count }}명 이상", "{{ user }} voted: {{ option }}": "{{ user }} 투표했습니다: {{ option }}", "{{count}} Audios_many": "{{count}}개의 오디오", "{{count}} Audios_one": "{{count}}개의 오디오", @@ -178,63 +237,23 @@ "{{count}} Photos_many": "{{count}}장의 사진", "{{count}} Photos_one": "{{count}}장의 사진", "{{count}} Photos_other": "{{count}}장의 사진", - "{{count}} Voice messages_many": "{{count}}개의 음성 메시지", - "{{count}} Voice messages_one": "{{count}}개의 음성 메시지", - "{{count}} Voice messages_other": "{{count}}개의 음성 메시지", + "{{count}} Reactions_many": "{{count}}개의 반응", + "{{count}} Reactions_one": "{{count}}개의 반응", + "{{count}} Reactions_other": "{{count}}개의 반응", "{{count}} Videos_many": "{{count}}개의 동영상", "{{count}} Videos_one": "{{count}}개의 동영상", "{{count}} Videos_other": "{{count}}개의 동영상", + "{{count}} Voice messages_many": "{{count}}개의 음성 메시지", + "{{count}} Voice messages_one": "{{count}}개의 음성 메시지", + "{{count}} Voice messages_other": "{{count}}개의 음성 메시지", + "{{count}} new messages": "{{count}} 새로운 메시지", + "{{count}} new threads": "{{count}} 새로운 스레드", + "{{count}} unread": "{{count}} 읽지 않은", "{{count}} votes_many": "{{count}} 표", "{{count}} votes_one": "{{count}} 표", "{{count}} votes_other": "{{count}} 표", - "🏙 Attachment...": "🏙 부착...", - "You have not granted access to the photo library.": "사진 라이브러리에 대한 접근 권한이 없습니다.", - "Change in Settings": "설정에서 변경", - "Create a poll and let everyone vote": "투표를 만들어 모두가 투표하게 하세요", - "Open Camera": "카메라 열기", - "Open Files": "파일 열기", - "Select files to share": "공유할 파일 선택", - "Take a photo and share": "사진을 찍고 공유", - "Take a video and share": "동영상을 촬영하고 공유", - "You have not granted access to your camera": "카메라 접근 권한이 없습니다", - "{{count}} Reactions_many": "{{count}}개의 반응", - "{{count}} Reactions_one": "{{count}}개의 반응", - "{{count}} Reactions_other": "{{count}}개의 반응", - "Tap to remove": "탭하여 제거", - "Draft": "초안", - "Reminder set": "리마인더 설정", - "Also sent in channel": "채널에도 전송", - "Replied to a thread": "스레드에 답장", - "View": "보기", - "Reminder overdue": "리마인더 만료", - "Poll has ended": "투표 종료됨", - "Reply to a message to start a thread": "메시지에 답장하여 스레드 시작", - "Couldn't load new threads. Tap to retry": "새로운 스레드를 로드할 수 없습니다. 탭하여 다시 시도", - "{{count}} new threads": "{{count}} 새로운 스레드", - "No conversations yet": "아직 대화가 없습니다", - "Are you sure you want to delete this group? This can't be undone.": "이 그룹을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", - "Are you sure you want to delete this chat? This can't be undone.": "이 채팅을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", - "Delete chat": "채팅 삭제", - "Delete group": "그룹 삭제", - "Archive Chat": "채팅 보관", - "Archive Group": "그룹 보관", - "Delete Chat": "채팅 삭제", - "Delete Group": "그룹 삭제", - "Leave Chat": "채팅 나가기", - "Leave Group": "그룹 나가기", - "Mute Group": "그룹 음소거", - "Offline": "오프라인", - "Online": "온라인", - "Unarchive Chat": "채팅 보관 해제", - "Unarchive Group": "그룹 보관 해제", - "Unmute Group": "그룹 음소거 해제", + "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}}명, {{onlineCount}}명 온라인", "{{memberCount}} members, {{onlineCount}} online_one": "{{memberCount}}명, {{onlineCount}}명 온라인", "{{memberCount}} members, {{onlineCount}} online_other": "{{memberCount}}명, {{onlineCount}}명 온라인", - "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}}명, {{onlineCount}}명 온라인", - "{{count}} unread": "{{count}} 읽지 않은", - "{{count}} new messages": "{{count}} 새로운 메시지", - "Unsupported Attachment": "지원하지 않는 첨부파일", - "+{{count}} More Options_one": "+옵션 {{count}}개 더", - "+{{count}} More Options_other": "+옵션 {{count}}개 더", - "+{{count}} More Options_many": "+옵션 {{count}}개 더" + "🏙 Attachment...": "🏙 부착..." } diff --git a/package/src/i18n/nl.json b/package/src/i18n/nl.json index 10811006cb..d6002b8bd6 100644 --- a/package/src/i18n/nl.json +++ b/package/src/i18n/nl.json @@ -1,4 +1,7 @@ { + "+{{count}} More Options_many": "+{{count}} extra opties", + "+{{count}} More Options_one": "+{{count}} extra optie", + "+{{count}} More Options_other": "+{{count}} extra opties", "+{{count}}_many": "+{{count}}", "+{{count}}_one": "+{{count}}", "+{{count}}_other": "+{{count}}", @@ -11,8 +14,13 @@ "Allow access to your Gallery": "Geef toegang tot uw galerij", "Allow camera access in device settings": "Sta cameratoegang toe in de apparaatinstellingen", "Also send to channel": "Stuur ook naar kanaal", + "Also sent in channel": "Also sent in channel", "Anonymous": "Anoniem", "Anonymous voting": "Anonieme peiling", + "Archive Chat": "Chat archiveren", + "Archive Group": "Groep archiveren", + "Are you sure you want to delete this chat? This can't be undone.": "Weet je zeker dat je deze chat wilt verwijderen? Dit kan niet ongedaan worden gemaakt.", + "Are you sure you want to delete this group? This can't be undone.": "Weet je zeker dat je deze groep wilt verwijderen? Dit kan niet ongedaan worden gemaakt.", "Are you sure you want to permanently delete this message?": "Weet u zeker dat u dit bericht definitief wilt verwijderen?", "Are you sure?": "Weet je het zeker?", "Ask a question": "Stel een vraag", @@ -21,23 +29,32 @@ "Block User": "Blokkeer Gebruiker", "Cancel": "Annuleer", "Cannot Flag Message": "Kan bericht niet rapporteren", + "Change in Settings": "Wijzigen in Instellingen", + "Choose between 2–10 options": "Kies tussen 2 en 10 opties", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Denk na over hoe jouw opmerking anderen zou kunnen laten voelen en zorg ervoor dat je onze Community-richtlijnen volgt", "Copy Message": "Bericht kopiëren", + "Couldn't load new threads. Tap to retry": "Kan nieuwe threads niet laden. Tik om opnieuw te proberen", "Create Poll": "Peiling aanmaken", + "Create a poll and let everyone vote": "Maak een peiling en laat iedereen stemmen", "Delete": "Verwijderen", + "Delete Chat": "Chat verwijderen", + "Delete Group": "Groep verwijderen", "Delete Message": "Verwijder bericht", + "Delete chat": "Chat verwijderen", "Delete for me": "Verwijder voor mij", + "Delete group": "Groep verwijderen", "Device camera is used to take photos or videos.": "De camera van het apparaat wordt gebruikt om foto's of video's te maken.", "Device gallery permissions is used to take photos or videos.": "Apparaatgallerijmachtigingen worden gebruikt om foto’s of video’s te maken.", "Do you want to send a copy of this message to a moderator for further investigation?": "Wil je een kopie van dit bericht naar een moderator sturen voor verder onderzoek?", + "Draft": "Ontwerp", "Due since {{ dueSince }}": "Vervaldatum sinds {{ dueSince }}", "Edit Message": "Pas bericht aan", "Edited": "Bewerkt", "Editing Message": "Bericht aanpassen", "Emoji matching": "Emoji-overeenkomsten", "Empty message...": "Leeg bericht...", - "Enter a new option": "Voer een nieuwe optie in", "End Vote": "Einde stemronde", + "Enter a new option": "Voer een nieuwe optie in", "Error loading": "Probleem bij het laden", "Error loading channel list...": "Probleem bij het laden van de kanalen...", "Error loading messages for this channel...": "Probleem bij het laden van de berichten in dit kanaal...", @@ -55,8 +72,11 @@ "Hold to start recording.": "Houd vast om opname te starten.", "How about sending your first message to a friend?": "Wat dacht je ervan om je eerste bericht naar een vriend te sturen?", "Instant Commands": "Directe Opdrachten", + "Leave Chat": "Chat verlaten", + "Leave Group": "Groep verlaten", "Let others add options": "Laat anderen opties toevoegen", "Let's start chatting!": "Laten we beginnen met chatten!", + "Limit votes per person": "Beperk stemmen per persoon", "Links are disabled": "Het versturen van links staat uit", "Live Location": "Live locatie", "Loading channels...": "Kanalen aan het laden...", @@ -68,24 +88,29 @@ "Maximum number of files reached": "Maximaal aantal bestanden bereikt", "Message Reactions": "Bericht Reacties", "Message deleted": "Bericht verwijderd", + "Message failed to send": "Bericht niet verzonden", "Message flagged": "Bericht gemarkeerd", "Multiple votes": "Meerdere stemmen", - "Network error": "Netwerkfout", - "Select more than one option": "Selecteer meer dan één optie", - "Limit votes per person": "Beperk stemmen per persoon", - "Choose between 2–10 options": "Kies tussen 2 en 10 opties", + "Mute Group": "Groep dempen", "Mute User": "Gebruiker dempen", + "Network error": "Netwerkfout", "No chats here yet…": "Nog geen chats hier…", + "No conversations yet": "Nog geen gesprekken", "No items exist": "Er zijn geen items", + "No messages yet": "Nog geen berichten", "No threads here yet": "Hier zijn nog geen threads", "Not supported": "niet ondersteund", "Nothing yet...": "Nog niets...", + "Offline": "Offline", "Ok": "Oké", + "Online": "Online", "Only visible to you": "Alleen zichtbaar voor jou", + "Open Camera": "Camera openen", + "Open Files": "Bestanden openen", "Open Settings": "Open instellingen", "Option": "Optie", - "Option {{count}}": "Optie {{count}}", "Option already exists": "Optie bestaat al", + "Option {{count}}": "Optie {{count}}", "Options": "Opties", "Photo": "Foto", "Photos and Videos": "Foto's en video's", @@ -97,16 +122,23 @@ "Poll Comments": "Peiling reacties", "Poll Options": "Peiling opties", "Poll Results": "Peiling resultaten", + "Poll has ended": "Stemmen beëindigd", "Questions": "Vragen", "Reconnecting...": "Opnieuw Verbinding Maken...", + "Reminder overdue": "Reminder overdue", + "Reminder set": "Reminder set", + "Replied to a thread": "Replied to a thread", "Reply": "Antwoord", - "Reply to {{name}}": "Antwoord aan {{name}}", "Reply to Message": "Beantwoord bericht", + "Reply to a message to start a thread": "Antwoord op een bericht om een thread te starten", + "Reply to {{name}}": "Antwoord aan {{name}}", "Resend": "Opnieuw versturen", "Retry Upload": "Uploaden opnieuw proberen", "SEND": "VERZENDEN", "Search": "Zoeken", "Select More Photos": "Selecteer Meer foto's", + "Select files to share": "Selecteer bestanden om te delen", + "Select more than one option": "Selecteer meer dan één optie", "Select one": "Kies één", "Select one or more": "Kies één of meer", "Select up to {{count}}_many": "Selecteer tot {{count}}", @@ -120,29 +152,61 @@ "Slow mode ON": "Langzame modus aan", "Slow mode, wait {{seconds}}s...": "Langzame modus, wacht {{seconds}}s...", "Suggest an option": "Stel een optie voor", + "Take a photo and share": "Maak een foto en deel deze", + "Take a video and share": "Maak een video en deel deze", + "Tap to remove": "Tik om te verwijderen", "The message has been reported to a moderator.": "Het bericht is gerapporteerd aan een moderator.", "The source message was deleted": "Het oorspronkelijke bericht is verwijderd", "Thinking...": "Aan het denken...", "This reply was deleted": "Deze reactie is verwijderd", "Thread Reply": "Discussie beantwoorden", "Type a number from 2 to 10": "Typ een getal van 2 tot 10", + "Typing": "Typen", + "Unarchive Chat": "Chat uit archief halen", + "Unarchive Group": "Groep uit archief halen", "Unban User": "Gebruiker Deblokeren", "Unblock User": "Deblokkeer gebruiker", "Unknown User": "Onbekende gebruiker", + "Unmute Group": "Dempen van groep opheffen", "Unmute User": "Dempen van gebruiker opheffen", "Unpin from Conversation": "Losmaken van gesprek", "Unread Messages": "Ongelezen Berichten", + "Unsupported Attachment": "Ondersteunde bijlage", "Update your comment": "Werk je reactie bij", "Video": "Video", + "View": "View", "View Results": "Bekijk resultaten", "View {{count}} comments_many": "Bekijk {{count}} reacties", "View {{count}} comments_one": "Bekijk {{count}} reactie", "View {{count}} comments_other": "Bekijk {{count}} reacties", "Voice message": "Spraakbericht", "Voice message ({{duration}})": "Spraakbericht ({{duration}})", - "Your comment": "Jouw reactie", "You": "U", "You can't send messages in this channel": "Je kan geen berichten sturen in dit kanaal", + "You have not granted access to the photo library.": "Je hebt geen toegang tot de fotobibliotheek verleend.", + "You have not granted access to your camera": "Je hebt geen toegang tot je camera verleend", + "You voted: {{ option }}": "Je hebt gestemd: {{ option }}", + "Your comment": "Jouw reactie", + "and {{ count }} others": "{{ count }} anderen", + "aria/AI is generating": "AI is generating", + "aria/AI is thinking": "AI is thinking", + "aria/Avatar of {{name}}": "Avatar of {{name}}", + "aria/Connected": "Connected", + "aria/Delivered": "Delivered", + "aria/Loading": "Loading", + "aria/Loading failed": "Loading failed", + "aria/New message from {{user}}": "New message from {{user}}", + "aria/Offline": "Offline", + "aria/Open message actions": "Open message actions", + "aria/Reaction {{emoji}} by {{count}} users": "Reaction {{emoji}} by {{count}} users", + "aria/Read": "Read", + "aria/Reconnecting": "Reconnecting", + "aria/Reply to {{user}}": "Reply to {{user}}", + "aria/Send message": "Send message", + "aria/Sending": "Sending", + "aria/Sent": "Sent", + "aria/Voice message recording. Hold to record.": "Voice message recording. Hold to record.", + "aria/{{count}} new messages": "{{count}} new messages", "duration/Location end at": "{{ milliseconds | durationFormatter(withSuffix: false) }}", "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", @@ -158,16 +222,11 @@ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Gisteren]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Morgen]\",\"nextWeek\":\"dddd [om] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} en {{ nonSelfUserLength }} anderen zijn aan het typen", + "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} en {{ secondUser }} zijn aan het typen", "{{ index }} of {{ photoLength }}": "{{ index }} van {{ photoLength }}", + "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} mensen zijn aan het typen", "{{ replyCount }} Replies": "{{ replyCount }} Antwoorden", "{{ user }} is typing": "{{ user }} is aan het typen", - "You voted: {{ option }}": "Je hebt gestemd: {{ option }}", - "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} en {{ secondUser }} zijn aan het typen", - "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} mensen zijn aan het typen", - "Typing": "Typen", - "No messages yet": "Nog geen berichten", - "Message failed to send": "Bericht niet verzonden", - "and {{ count }} others": "{{ count }} anderen", "{{ user }} voted: {{ option }}": "{{ user }} heeft gestemd: {{ option }}", "{{count}} Audios_many": "{{count}} audio's", "{{count}} Audios_one": "{{count}} audio", @@ -178,63 +237,23 @@ "{{count}} Photos_many": "{{count}} foto's", "{{count}} Photos_one": "{{count}} foto", "{{count}} Photos_other": "{{count}} foto's", - "{{count}} Voice messages_many": "{{count}} spraakberichten", - "{{count}} Voice messages_one": "{{count}} spraakbericht", - "{{count}} Voice messages_other": "{{count}} spraakberichten", + "{{count}} Reactions_many": "{{count}} reacties", + "{{count}} Reactions_one": "{{count}} reactie", + "{{count}} Reactions_other": "{{count}} reacties", "{{count}} Videos_many": "{{count}} video's", "{{count}} Videos_one": "{{count}} video", "{{count}} Videos_other": "{{count}} video's", + "{{count}} Voice messages_many": "{{count}} spraakberichten", + "{{count}} Voice messages_one": "{{count}} spraakbericht", + "{{count}} Voice messages_other": "{{count}} spraakberichten", + "{{count}} new messages": "{{count}} nieuwe berichten", + "{{count}} new threads": "{{count}} nieuwe threads", + "{{count}} unread": "{{count}} ongelezen", "{{count}} votes_many": "{{count}} stemmen", "{{count}} votes_one": "{{count}} stem", "{{count}} votes_other": "{{count}} stemmen", - "🏙 Attachment...": "🏙 Bijlage...", - "You have not granted access to the photo library.": "Je hebt geen toegang tot de fotobibliotheek verleend.", - "Change in Settings": "Wijzigen in Instellingen", - "Create a poll and let everyone vote": "Maak een peiling en laat iedereen stemmen", - "Open Camera": "Camera openen", - "Open Files": "Bestanden openen", - "Select files to share": "Selecteer bestanden om te delen", - "Take a photo and share": "Maak een foto en deel deze", - "Take a video and share": "Maak een video en deel deze", - "You have not granted access to your camera": "Je hebt geen toegang tot je camera verleend", - "{{count}} Reactions_many": "{{count}} reacties", - "{{count}} Reactions_one": "{{count}} reactie", - "{{count}} Reactions_other": "{{count}} reacties", - "Tap to remove": "Tik om te verwijderen", - "Draft": "Ontwerp", - "Reminder set": "Reminder set", - "Also sent in channel": "Also sent in channel", - "Replied to a thread": "Replied to a thread", - "View": "View", - "Reminder overdue": "Reminder overdue", - "Poll has ended": "Stemmen beëindigd", - "Reply to a message to start a thread": "Antwoord op een bericht om een thread te starten", - "Couldn't load new threads. Tap to retry": "Kan nieuwe threads niet laden. Tik om opnieuw te proberen", - "{{count}} new threads": "{{count}} nieuwe threads", - "No conversations yet": "Nog geen gesprekken", - "Are you sure you want to delete this group? This can't be undone.": "Weet je zeker dat je deze groep wilt verwijderen? Dit kan niet ongedaan worden gemaakt.", - "Are you sure you want to delete this chat? This can't be undone.": "Weet je zeker dat je deze chat wilt verwijderen? Dit kan niet ongedaan worden gemaakt.", - "Delete chat": "Chat verwijderen", - "Delete group": "Groep verwijderen", - "Archive Chat": "Chat archiveren", - "Archive Group": "Groep archiveren", - "Delete Chat": "Chat verwijderen", - "Delete Group": "Groep verwijderen", - "Leave Chat": "Chat verlaten", - "Leave Group": "Groep verlaten", - "Mute Group": "Groep dempen", - "Offline": "Offline", - "Online": "Online", - "Unarchive Chat": "Chat uit archief halen", - "Unarchive Group": "Groep uit archief halen", - "Unmute Group": "Dempen van groep opheffen", + "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} leden, {{onlineCount}} online", "{{memberCount}} members, {{onlineCount}} online_one": "{{memberCount}} lid, {{onlineCount}} online", "{{memberCount}} members, {{onlineCount}} online_other": "{{memberCount}} leden, {{onlineCount}} online", - "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} leden, {{onlineCount}} online", - "{{count}} unread": "{{count}} ongelezen", - "{{count}} new messages": "{{count}} nieuwe berichten", - "Unsupported Attachment": "Ondersteunde bijlage", - "+{{count}} More Options_one": "+{{count}} extra optie", - "+{{count}} More Options_other": "+{{count}} extra opties", - "+{{count}} More Options_many": "+{{count}} extra opties" + "🏙 Attachment...": "🏙 Bijlage..." } diff --git a/package/src/i18n/pt-br.json b/package/src/i18n/pt-br.json index 327684079c..bb652606e6 100644 --- a/package/src/i18n/pt-br.json +++ b/package/src/i18n/pt-br.json @@ -1,4 +1,7 @@ { + "+{{count}} More Options_many": "+{{count}} opções a mais", + "+{{count}} More Options_one": "+{{count}} opção a mais", + "+{{count}} More Options_other": "+{{count}} opções a mais", "+{{count}}_many": "+{{count}}", "+{{count}}_one": "+{{count}}", "+{{count}}_other": "+{{count}}", @@ -11,8 +14,13 @@ "Allow access to your Gallery": "Permitir acesso à sua Galeria", "Allow camera access in device settings": "Permitir acesso à câmera nas configurações do dispositivo", "Also send to channel": "Também enviar para o canal", + "Also sent in channel": "También enviado en el canal", "Anonymous": "Anônimo", "Anonymous voting": "Enquete anônima", + "Archive Chat": "Arquivar conversa", + "Archive Group": "Arquivar grupo", + "Are you sure you want to delete this chat? This can't be undone.": "Tem certeza de que deseja excluir esta conversa? Esta ação não pode ser desfeita.", + "Are you sure you want to delete this group? This can't be undone.": "Tem certeza de que deseja excluir este grupo? Esta ação não pode ser desfeita.", "Are you sure you want to permanently delete this message?": "Tem certeza de que deseja excluir esta mensagem permanentemente?", "Are you sure?": "Tem certeza?", "Ask a question": "Fazer uma pergunta", @@ -21,23 +29,32 @@ "Block User": "Bloquear Usuário", "Cancel": "Cancelar", "Cannot Flag Message": "Não é possível reportar a mensagem", + "Change in Settings": "Alterar nos Ajustes", + "Choose between 2–10 options": "Escolha entre 2 e 10 opções", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Considere como seu comentário pode fazer os outros se sentirem e certifique-se de seguir nossas Diretrizes da Comunidade", "Copy Message": "Copiar Mensagem", + "Couldn't load new threads. Tap to retry": "Não foi possível carregar novos tópicos. Toque para tentar novamente", "Create Poll": "Criar enquete", + "Create a poll and let everyone vote": "Crie uma enquete e deixe todos votarem", "Delete": "Excluir", + "Delete Chat": "Excluir conversa", + "Delete Group": "Excluir grupo", "Delete Message": "Excluir Mensagem", + "Delete chat": "Excluir conversa", "Delete for me": "Excluir para mim", + "Delete group": "Excluir grupo", "Device camera is used to take photos or videos.": "A câmera do dispositivo é usada para tirar fotos ou vídeos.", "Device gallery permissions is used to take photos or videos.": "As permissões da galeria do dispositivo são usadas para tirar fotos ou vídeos.", "Do you want to send a copy of this message to a moderator for further investigation?": "Deseja enviar uma cópia desta mensagem para um moderador para investigação adicional?", + "Draft": "Rascunho", "Due since {{ dueSince }}": "Vencido desde {{ dueSince }}", "Edit Message": "Editar Mensagem", "Edited": "Editado", "Editing Message": "Editando Mensagem", "Emoji matching": "Correspondência de Emoji", "Empty message...": "Mensagem vazia...", - "Enter a new option": "Digite uma nova opção", "End Vote": "Encerrar votação", + "Enter a new option": "Digite uma nova opção", "Error loading": "Erro ao carregar", "Error loading channel list...": "Erro ao carregar lista de canais...", "Error loading messages for this channel...": "Erro ao carregar mensagens para este canal...", @@ -55,8 +72,11 @@ "Hold to start recording.": "Mantenha pressionado para começar a gravar.", "How about sending your first message to a friend?": "Que tal enviar sua primeira mensagem para um amigo?", "Instant Commands": "Comandos Instantâneos", + "Leave Chat": "Sair da conversa", + "Leave Group": "Sair do grupo", "Let others add options": "Permitir que outros adicionem opções", "Let's start chatting!": "Vamos começar a conversar!", + "Limit votes per person": "Limite os votos por pessoa", "Links are disabled": "Links estão desabilitados", "Live Location": "Localização ao vivo", "Loading channels...": "Carregando canais...", @@ -68,24 +88,29 @@ "Maximum number of files reached": "Número máximo de arquivos atingido", "Message Reactions": "Reações à Mensagem", "Message deleted": "Mensagem excluída", + "Message failed to send": "Mensagem não enviada", "Message flagged": "Mensagem sinalizada", "Multiple votes": "Votos múltiplos", - "Network error": "Erro de rede", - "Select more than one option": "Selecione mais de uma opção", - "Limit votes per person": "Limite os votos por pessoa", - "Choose between 2–10 options": "Escolha entre 2 e 10 opções", + "Mute Group": "Silenciar grupo", "Mute User": "Silenciar Usuário", + "Network error": "Erro de rede", "No chats here yet…": "Ainda não há chats aqui...", + "No conversations yet": "Ainda não há conversas", "No items exist": "Nenhum item", + "No messages yet": "Ainda não há mensagens", "No threads here yet": "Ainda não há tópicos aqui", "Not supported": "Não suportado", "Nothing yet...": "Nada ainda...", + "Offline": "Offline", "Ok": "Ok", + "Online": "Online", "Only visible to you": "Apenas visível para você", + "Open Camera": "Abrir camera", + "Open Files": "Abrir arquivos", "Open Settings": "Abrir Configurações", "Option": "Opção", - "Option {{count}}": "Opção {{count}}", "Option already exists": "A opção já existe", + "Option {{count}}": "Opção {{count}}", "Options": "Opções", "Photo": "Foto", "Photos and Videos": "Fotos e Vídeos", @@ -97,16 +122,23 @@ "Poll Comments": "Comentários da enquete", "Poll Options": "Opções da enquete", "Poll Results": "Resultados da enquete", + "Poll has ended": "Votação encerrada", "Questions": "Perguntas", "Reconnecting...": "Reconectando...", + "Reminder overdue": "Recordatorio vencido", + "Reminder set": "Recordatorio establecido", + "Replied to a thread": "Respondido a un hilo", "Reply": "Responder", - "Reply to {{name}}": "Responder a {{name}}", "Reply to Message": "Responder à Mensagem", + "Reply to a message to start a thread": "Responder a uma mensagem para iniciar um tópico", + "Reply to {{name}}": "Responder a {{name}}", "Resend": "Reenviar", "Retry Upload": "Tentar upload novamente", "SEND": "ENVIAR", "Search": "Pesquisar", "Select More Photos": "Selecionar Mais Fotos", + "Select files to share": "Selecione arquivos para compartilhar", + "Select more than one option": "Selecione mais de uma opção", "Select one": "Selecione uma", "Select one or more": "Selecione uma ou mais", "Select up to {{count}}_many": "Selecione até {{count}}", @@ -120,29 +152,61 @@ "Slow mode ON": "Modo Lento ATIVADO", "Slow mode, wait {{seconds}}s...": "Modo lento, aguarde {{seconds}}s...", "Suggest an option": "Sugerir uma opção", + "Take a photo and share": "Tire uma foto e compartilhe", + "Take a video and share": "Grave um video e compartilhe", + "Tap to remove": "Toque para remover", "The message has been reported to a moderator.": "A mensagem foi relatada a um moderador.", "The source message was deleted": "A mensagem original foi excluída", "Thinking...": "Pensando...", "This reply was deleted": "Esta resposta foi excluída", "Thread Reply": "Respostas de Tópico", "Type a number from 2 to 10": "Digite um número de 2 a 10", + "Typing": "Digitando", + "Unarchive Chat": "Desarquivar conversa", + "Unarchive Group": "Desarquivar grupo", "Unban User": "Desbanir Usuário", "Unblock User": "Desbloquear Usuário", "Unknown User": "Usuário Desconhecido", + "Unmute Group": "Remover grupo do modo silencioso", "Unmute User": "Remover usuário do modo silencioso", "Unpin from Conversation": "Desmarcar como fixado na conversa", "Unread Messages": "Mensagens não lidas", + "Unsupported Attachment": "Anexo não suportado", "Update your comment": "Atualize seu comentário", "Video": "Vídeo", + "View": "Ver", "View Results": "Ver resultados", "View {{count}} comments_many": "Ver {{count}} comentários", "View {{count}} comments_one": "Ver {{count}} comentário", "View {{count}} comments_other": "Ver {{count}} comentários", "Voice message": "Mensagem de voz", "Voice message ({{duration}})": "Mensagem de voz ({{duration}})", - "Your comment": "Seu comentário", "You": "Você", "You can't send messages in this channel": "Você não pode enviar mensagens neste canal", + "You have not granted access to the photo library.": "Você não concedeu acesso à biblioteca de fotos.", + "You have not granted access to your camera": "Você não concedeu acesso à sua câmera", + "You voted: {{ option }}": "Você votou: {{ option }}", + "Your comment": "Seu comentário", + "and {{ count }} others": "{{ count }} outros", + "aria/AI is generating": "AI is generating", + "aria/AI is thinking": "AI is thinking", + "aria/Avatar of {{name}}": "Avatar of {{name}}", + "aria/Connected": "Connected", + "aria/Delivered": "Delivered", + "aria/Loading": "Loading", + "aria/Loading failed": "Loading failed", + "aria/New message from {{user}}": "New message from {{user}}", + "aria/Offline": "Offline", + "aria/Open message actions": "Open message actions", + "aria/Reaction {{emoji}} by {{count}} users": "Reaction {{emoji}} by {{count}} users", + "aria/Read": "Read", + "aria/Reconnecting": "Reconnecting", + "aria/Reply to {{user}}": "Reply to {{user}}", + "aria/Send message": "Send message", + "aria/Sending": "Sending", + "aria/Sent": "Sent", + "aria/Voice message recording. Hold to record.": "Voice message recording. Hold to record.", + "aria/{{count}} new messages": "{{count}} new messages", "duration/Location end at": "{{ milliseconds | durationFormatter(withSuffix: false) }}", "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", @@ -158,16 +222,11 @@ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Ontem]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Amanhã]\",\"nextWeek\":\"dddd [às] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} e mais {{ nonSelfUserLength }} pessoa(s) estão digitando", + "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} e {{ secondUser }} estão digitando", "{{ index }} of {{ photoLength }}": "{{ index }} de {{ photoLength }}", + "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} pessoas estão digitando", "{{ replyCount }} Replies": "{{ replyCount }} Respostas", "{{ user }} is typing": "{{ user }} está digitando", - "You voted: {{ option }}": "Você votou: {{ option }}", - "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} e {{ secondUser }} estão digitando", - "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} pessoas estão digitando", - "Typing": "Digitando", - "No messages yet": "Ainda não há mensagens", - "Message failed to send": "Mensagem não enviada", - "and {{ count }} others": "{{ count }} outros", "{{ user }} voted: {{ option }}": "{{ user }} votou: {{ option }}", "{{count}} Audios_many": "{{count}} áudios", "{{count}} Audios_one": "{{count}} áudio", @@ -178,63 +237,23 @@ "{{count}} Photos_many": "{{count}} fotos", "{{count}} Photos_one": "{{count}} foto", "{{count}} Photos_other": "{{count}} fotos", - "{{count}} Voice messages_many": "{{count}} mensagens de voz", - "{{count}} Voice messages_one": "{{count}} mensagem de voz", - "{{count}} Voice messages_other": "{{count}} mensagens de voz", + "{{count}} Reactions_many": "{{count}} reações", + "{{count}} Reactions_one": "{{count}} reação", + "{{count}} Reactions_other": "{{count}} reações", "{{count}} Videos_many": "{{count}} vídeos", "{{count}} Videos_one": "{{count}} vídeo", "{{count}} Videos_other": "{{count}} vídeos", + "{{count}} Voice messages_many": "{{count}} mensagens de voz", + "{{count}} Voice messages_one": "{{count}} mensagem de voz", + "{{count}} Voice messages_other": "{{count}} mensagens de voz", + "{{count}} new messages": "{{count}} novas mensagens", + "{{count}} new threads": "{{count}} novos tópicos", + "{{count}} unread": "{{count}} não lidas", "{{count}} votes_many": "{{count}} votos", "{{count}} votes_one": "{{count}} voto", "{{count}} votes_other": "{{count}} votos", - "🏙 Attachment...": "🏙 Anexo...", - "You have not granted access to the photo library.": "Você não concedeu acesso à biblioteca de fotos.", - "Change in Settings": "Alterar nos Ajustes", - "Create a poll and let everyone vote": "Crie uma enquete e deixe todos votarem", - "Open Camera": "Abrir camera", - "Open Files": "Abrir arquivos", - "Select files to share": "Selecione arquivos para compartilhar", - "Take a photo and share": "Tire uma foto e compartilhe", - "Take a video and share": "Grave um video e compartilhe", - "You have not granted access to your camera": "Você não concedeu acesso à sua câmera", - "{{count}} Reactions_many": "{{count}} reações", - "{{count}} Reactions_one": "{{count}} reação", - "{{count}} Reactions_other": "{{count}} reações", - "Tap to remove": "Toque para remover", - "Draft": "Rascunho", - "Reminder set": "Recordatorio establecido", - "Also sent in channel": "También enviado en el canal", - "Replied to a thread": "Respondido a un hilo", - "View": "Ver", - "Reminder overdue": "Recordatorio vencido", - "Poll has ended": "Votação encerrada", - "Reply to a message to start a thread": "Responder a uma mensagem para iniciar um tópico", - "Couldn't load new threads. Tap to retry": "Não foi possível carregar novos tópicos. Toque para tentar novamente", - "{{count}} new threads": "{{count}} novos tópicos", - "No conversations yet": "Ainda não há conversas", - "Are you sure you want to delete this group? This can't be undone.": "Tem certeza de que deseja excluir este grupo? Esta ação não pode ser desfeita.", - "Are you sure you want to delete this chat? This can't be undone.": "Tem certeza de que deseja excluir esta conversa? Esta ação não pode ser desfeita.", - "Delete chat": "Excluir conversa", - "Delete group": "Excluir grupo", - "Archive Chat": "Arquivar conversa", - "Archive Group": "Arquivar grupo", - "Delete Chat": "Excluir conversa", - "Delete Group": "Excluir grupo", - "Leave Chat": "Sair da conversa", - "Leave Group": "Sair do grupo", - "Mute Group": "Silenciar grupo", - "Offline": "Offline", - "Online": "Online", - "Unarchive Chat": "Desarquivar conversa", - "Unarchive Group": "Desarquivar grupo", - "Unmute Group": "Remover grupo do modo silencioso", + "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} membros, {{onlineCount}} online", "{{memberCount}} members, {{onlineCount}} online_one": "{{memberCount}} membro, {{onlineCount}} online", "{{memberCount}} members, {{onlineCount}} online_other": "{{memberCount}} membros, {{onlineCount}} online", - "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} membros, {{onlineCount}} online", - "{{count}} unread": "{{count}} não lidas", - "{{count}} new messages": "{{count}} novas mensagens", - "Unsupported Attachment": "Anexo não suportado", - "+{{count}} More Options_one": "+{{count}} opção a mais", - "+{{count}} More Options_other": "+{{count}} opções a mais", - "+{{count}} More Options_many": "+{{count}} opções a mais" + "🏙 Attachment...": "🏙 Anexo..." } diff --git a/package/src/i18n/ru.json b/package/src/i18n/ru.json index aa73598ab9..c93221977c 100644 --- a/package/src/i18n/ru.json +++ b/package/src/i18n/ru.json @@ -1,4 +1,7 @@ { + "+{{count}} More Options_many": "+ещё {{count}} вариантов", + "+{{count}} More Options_one": "+ещё {{count}} вариант", + "+{{count}} More Options_other": "+ещё {{count}} варианта", "+{{count}}_many": "+{{count}}", "+{{count}}_one": "+{{count}}", "+{{count}}_other": "+{{count}}", @@ -11,8 +14,13 @@ "Allow access to your Gallery": "Разрешить доступ к вашей галерее", "Allow camera access in device settings": "Разрешите доступ к камере в настройках устройства.", "Also send to channel": "Также отправить на канал", + "Also sent in channel": "Также отправлено в канал", "Anonymous": "Анонимный", "Anonymous voting": "Анонимный опрос", + "Archive Chat": "Архивировать чат", + "Archive Group": "Архивировать группу", + "Are you sure you want to delete this chat? This can't be undone.": "Вы уверены, что хотите удалить этот чат? Это действие нельзя отменить.", + "Are you sure you want to delete this group? This can't be undone.": "Вы уверены, что хотите удалить эту группу? Это действие нельзя отменить.", "Are you sure you want to permanently delete this message?": "Вы действительно хотите удалить это сообщение без возможности восстановления?", "Are you sure?": "Вы уверены?", "Ask a question": "Задайте вопрос", @@ -21,23 +29,32 @@ "Block User": "Заблокировать пользователя", "Cancel": "Отмена", "Cannot Flag Message": "Невозможно пожаловаться на сообщение", + "Change in Settings": "Изменить в настройках", + "Choose between 2–10 options": "Выберите от 2 до 10 вариантов", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Обдумайте, как ваш комментарий может повлиять на других, и убедитесь, что вы следуете нашим правилам сообщества", "Copy Message": "Копировать сообщение", + "Couldn't load new threads. Tap to retry": "Не удалось загрузить новые темы. Нажмите, чтобы попробовать снова", "Create Poll": "Создать опрос", + "Create a poll and let everyone vote": "Создайте опрос и дайте всем проголосовать", "Delete": "удалять", + "Delete Chat": "Удалить чат", + "Delete Group": "Удалить группу", "Delete Message": "Удалить сообщение", + "Delete chat": "Удалить чат", "Delete for me": "Удалить для себя", + "Delete group": "Удалить группу", "Device camera is used to take photos or videos.": "Камера устройства используется для съемки фотографий или видео.", "Device gallery permissions is used to take photos or videos.": "Разрешения галереи устройства используются для съемки фото или видео.", "Do you want to send a copy of this message to a moderator for further investigation?": "Вы хотите отправить копию этого сообщения модератору для дальнейшего изучения?", + "Draft": "Черновик", "Due since {{ dueSince }}": "Срок с {{ dueSince }}", "Edit Message": "Редактировать сообщение", "Edited": "Отредактировано", "Editing Message": "Редактирование сообщения", "Emoji matching": "Соответствие эмодзи", "Empty message...": "Пустое сообщение...", - "Enter a new option": "Введите новый вариант", "End Vote": "Завершить голосование", + "Enter a new option": "Введите новый вариант", "Error loading": "Ошибка при загрузке", "Error loading channel list...": "Ошибка загрузки списка каналов...", "Error loading messages for this channel...": "Ошибка загрузки сообщений для этого канала...", @@ -55,8 +72,11 @@ "Hold to start recording.": "Удерживайте, чтобы начать запись.", "How about sending your first message to a friend?": "Как насчет отправки первого сообщения другу?", "Instant Commands": "Мгновенные Команды", + "Leave Chat": "Покинуть чат", + "Leave Group": "Покинуть группу", "Let others add options": "Разрешить другим добавлять варианты", "Let's start chatting!": "Давайте начнем общаться!", + "Limit votes per person": "Ограничить количество голосов на человека", "Links are disabled": "Ссылки отключены", "Live Location": "Трансляция местоположения", "Loading channels...": "Загружаю каналы...", @@ -68,24 +88,29 @@ "Maximum number of files reached": "Достигнуто максимальное количество файлов", "Message Reactions": "Сообщения Реакции", "Message deleted": "Сообщение удалено", + "Message failed to send": "Сообщение не отправлено", "Message flagged": "Сообщение отмечено", "Multiple votes": "Несколько голосов", - "Network error": "Ошибка сети", - "Select more than one option": "Выберите больше одного варианта", - "Limit votes per person": "Ограничить количество голосов на человека", - "Choose between 2–10 options": "Выберите от 2 до 10 вариантов", + "Mute Group": "Отключить группу", "Mute User": "Отключить пользователя", + "Network error": "Ошибка сети", "No chats here yet…": "Здесь пока нет чатов…", + "No conversations yet": "Нет чатов", "No items exist": "Нет элементов", + "No messages yet": "Нет сообщений", "No threads here yet": "Здесь пока нет потоков", "Not supported": "не поддерживается", "Nothing yet...": "Пока ничего нет...", + "Offline": "Не в сети", "Ok": "Oк", + "Online": "В сети", "Only visible to you": "Видно только вам", + "Open Camera": "Открыть камеру", + "Open Files": "Открыть файлы", "Open Settings": "Открыть настройки", "Option": "Вариант", - "Option {{count}}": "Вариант {{count}}", "Option already exists": "Вариант уже существует", + "Option {{count}}": "Вариант {{count}}", "Options": "Варианты", "Photo": "Фото", "Photos and Videos": "Фото и видео", @@ -97,16 +122,23 @@ "Poll Comments": "Комментарии к опросу", "Poll Options": "Варианты опроса", "Poll Results": "Результаты опроса", + "Poll has ended": "Голосование завершено", "Questions": "Вопросы", "Reconnecting...": "Переподключение...", + "Reminder overdue": "Напоминание просрочено", + "Reminder set": "Напоминание установлено", + "Replied to a thread": "Ответил на тему", "Reply": "Ответить", - "Reply to {{name}}": "Ответить пользователю {{name}}", "Reply to Message": "Ответить на сообщение", + "Reply to a message to start a thread": "Ответить на сообщение, чтобы начать тему", + "Reply to {{name}}": "Ответить пользователю {{name}}", "Resend": "Отправить", "Retry Upload": "Повторить загрузку", "SEND": "ОТПРАВИТЬ", "Search": "Поиск", "Select More Photos": "Выбрать больше фотографий", + "Select files to share": "Выберите файлы для отправки", + "Select more than one option": "Выберите больше одного варианта", "Select one": "Выберите один", "Select one or more": "Выберите один или несколько", "Select up to {{count}}_many": "Выберите до {{count}}", @@ -120,29 +152,61 @@ "Slow mode ON": "Медленный режим включен", "Slow mode, wait {{seconds}}s...": "Медленный режим, ожидайте {{seconds}}s...", "Suggest an option": "Предложить вариант", + "Take a photo and share": "Сделайте фото и поделитесь", + "Take a video and share": "Снимите видео и поделитесь", + "Tap to remove": "Нажмите, чтобы удалить", "The message has been reported to a moderator.": "Сообщение отправлено модератору.", "The source message was deleted": "Исходное сообщение было удалено", "Thinking...": "Думаю...", "This reply was deleted": "Этот ответ был удалён", "Thread Reply": "Тема Ответить", "Type a number from 2 to 10": "Введите число от 2 до 10", + "Typing": "Пишет", + "Unarchive Chat": "Разархивировать чат", + "Unarchive Group": "Разархивировать группу", "Unban User": "Разблокировать Пользователя", "Unblock User": "Разблокировать пользователя", "Unknown User": "Неизвестный пользователь", + "Unmute Group": "Включить группу", "Unmute User": "Включить микрофон", "Unpin from Conversation": "Открепить от беседы", "Unread Messages": "Непрочитанные Сообщения", + "Unsupported Attachment": "Неподдерживаемое вложение", "Update your comment": "Обновить ваш комментарий", "Video": "видео", + "View": "Посмотреть", "View Results": "Посмотреть результаты", "View {{count}} comments_many": "Посмотреть {{count}} комментария", "View {{count}} comments_one": "Посмотреть {{count}} комментарий", "View {{count}} comments_other": "Посмотреть {{count}} комментария", "Voice message": "Голосовое сообщение", "Voice message ({{duration}})": "Голосовое сообщение ({{duration}})", - "Your comment": "Ваш комментарий", "You": "Вы", "You can't send messages in this channel": "Вы не можете отправлять сообщения в этот канал", + "You have not granted access to the photo library.": "Вы не предоставили доступ к фотобиблиотеке.", + "You have not granted access to your camera": "Вы не предоставили доступ к вашей камере", + "You voted: {{ option }}": "Вы проголосовали: {{ option }}", + "Your comment": "Ваш комментарий", + "and {{ count }} others": "{{ count }} других", + "aria/AI is generating": "AI is generating", + "aria/AI is thinking": "AI is thinking", + "aria/Avatar of {{name}}": "Avatar of {{name}}", + "aria/Connected": "Connected", + "aria/Delivered": "Delivered", + "aria/Loading": "Loading", + "aria/Loading failed": "Loading failed", + "aria/New message from {{user}}": "New message from {{user}}", + "aria/Offline": "Offline", + "aria/Open message actions": "Open message actions", + "aria/Reaction {{emoji}} by {{count}} users": "Reaction {{emoji}} by {{count}} users", + "aria/Read": "Read", + "aria/Reconnecting": "Reconnecting", + "aria/Reply to {{user}}": "Reply to {{user}}", + "aria/Send message": "Send message", + "aria/Sending": "Sending", + "aria/Sent": "Sent", + "aria/Voice message recording. Hold to record.": "Voice message recording. Hold to record.", + "aria/{{count}} new messages": "{{count}} new messages", "duration/Location end at": "{{ milliseconds | durationFormatter(withSuffix: false) }}", "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", @@ -158,16 +222,11 @@ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Вчера]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Завтра]\",\"nextWeek\":\"dddd [в] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} и еще {{ nonSelfUserLength }} пишут", + "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} и {{ secondUser }} пишут", "{{ index }} of {{ photoLength }}": "{{ index }} из {{ photoLength }}", + "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} людей пишут", "{{ replyCount }} Replies": "{{ replyCount }} Ответов", "{{ user }} is typing": "{{ user }} пишет", - "You voted: {{ option }}": "Вы проголосовали: {{ option }}", - "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} и {{ secondUser }} пишут", - "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} людей пишут", - "Typing": "Пишет", - "No messages yet": "Нет сообщений", - "Message failed to send": "Сообщение не отправлено", - "and {{ count }} others": "{{ count }} других", "{{ user }} voted: {{ option }}": "{{ user }} проголосовал: {{ option }}", "{{count}} Audios_many": "{{count}} аудио", "{{count}} Audios_one": "{{count}} аудио", @@ -178,63 +237,23 @@ "{{count}} Photos_many": "{{count}} фото", "{{count}} Photos_one": "{{count}} фото", "{{count}} Photos_other": "{{count}} фото", - "{{count}} Voice messages_many": "{{count}} голосовых сообщений", - "{{count}} Voice messages_one": "{{count}} голосовое сообщение", - "{{count}} Voice messages_other": "{{count}} голосовых сообщений", + "{{count}} Reactions_many": "{{count}} реакций", + "{{count}} Reactions_one": "{{count}} реакция", + "{{count}} Reactions_other": "{{count}} реакций", "{{count}} Videos_many": "{{count}} видео", "{{count}} Videos_one": "{{count}} видео", "{{count}} Videos_other": "{{count}} видео", + "{{count}} Voice messages_many": "{{count}} голосовых сообщений", + "{{count}} Voice messages_one": "{{count}} голосовое сообщение", + "{{count}} Voice messages_other": "{{count}} голосовых сообщений", + "{{count}} new messages": "{{count}} новых сообщений", + "{{count}} new threads": "{{count}} новых тем", + "{{count}} unread": "{{count}} непрочитанных", "{{count}} votes_many": "{{count}} голосов", "{{count}} votes_one": "{{count}} голос", "{{count}} votes_other": "{{count}} голосов", - "🏙 Attachment...": "🏙 Вложение...", - "You have not granted access to the photo library.": "Вы не предоставили доступ к фотобиблиотеке.", - "Change in Settings": "Изменить в настройках", - "Create a poll and let everyone vote": "Создайте опрос и дайте всем проголосовать", - "Open Camera": "Открыть камеру", - "Open Files": "Открыть файлы", - "Select files to share": "Выберите файлы для отправки", - "Take a photo and share": "Сделайте фото и поделитесь", - "Take a video and share": "Снимите видео и поделитесь", - "You have not granted access to your camera": "Вы не предоставили доступ к вашей камере", - "{{count}} Reactions_many": "{{count}} реакций", - "{{count}} Reactions_one": "{{count}} реакция", - "{{count}} Reactions_other": "{{count}} реакций", - "Tap to remove": "Нажмите, чтобы удалить", - "Draft": "Черновик", - "Reminder set": "Напоминание установлено", - "Also sent in channel": "Также отправлено в канал", - "Replied to a thread": "Ответил на тему", - "View": "Посмотреть", - "Reminder overdue": "Напоминание просрочено", - "Poll has ended": "Голосование завершено", - "Reply to a message to start a thread": "Ответить на сообщение, чтобы начать тему", - "Couldn't load new threads. Tap to retry": "Не удалось загрузить новые темы. Нажмите, чтобы попробовать снова", - "{{count}} new threads": "{{count}} новых тем", - "No conversations yet": "Нет чатов", - "Are you sure you want to delete this group? This can't be undone.": "Вы уверены, что хотите удалить эту группу? Это действие нельзя отменить.", - "Are you sure you want to delete this chat? This can't be undone.": "Вы уверены, что хотите удалить этот чат? Это действие нельзя отменить.", - "Delete chat": "Удалить чат", - "Delete group": "Удалить группу", - "Archive Chat": "Архивировать чат", - "Archive Group": "Архивировать группу", - "Delete Chat": "Удалить чат", - "Delete Group": "Удалить группу", - "Leave Chat": "Покинуть чат", - "Leave Group": "Покинуть группу", - "Mute Group": "Отключить группу", - "Offline": "Не в сети", - "Online": "В сети", - "Unarchive Chat": "Разархивировать чат", - "Unarchive Group": "Разархивировать группу", - "Unmute Group": "Включить группу", + "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} участников, {{onlineCount}} онлайн", "{{memberCount}} members, {{onlineCount}} online_one": "{{memberCount}} участник, {{onlineCount}} онлайн", "{{memberCount}} members, {{onlineCount}} online_other": "{{memberCount}} участника, {{onlineCount}} онлайн", - "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} участников, {{onlineCount}} онлайн", - "{{count}} unread": "{{count}} непрочитанных", - "{{count}} new messages": "{{count}} новых сообщений", - "Unsupported Attachment": "Неподдерживаемое вложение", - "+{{count}} More Options_one": "+ещё {{count}} вариант", - "+{{count}} More Options_other": "+ещё {{count}} варианта", - "+{{count}} More Options_many": "+ещё {{count}} вариантов" + "🏙 Attachment...": "🏙 Вложение..." } diff --git a/package/src/i18n/tr.json b/package/src/i18n/tr.json index 25e4cd570d..260d45f665 100644 --- a/package/src/i18n/tr.json +++ b/package/src/i18n/tr.json @@ -1,4 +1,7 @@ { + "+{{count}} More Options_many": "+{{count}} seçenek daha", + "+{{count}} More Options_one": "+{{count}} seçenek daha", + "+{{count}} More Options_other": "+{{count}} seçenek daha", "+{{count}}_many": "+{{count}}", "+{{count}}_one": "+{{count}}", "+{{count}}_other": "+{{count}}", @@ -11,8 +14,13 @@ "Allow access to your Gallery": "Galerinize erişime izin verin", "Allow camera access in device settings": "Cihaz ayarlarında kamera erişimine izin ver", "Also send to channel": "Kanala da gönder", + "Also sent in channel": "Kanala da gönderildi", "Anonymous": "Anonim", "Anonymous voting": "Anonim anket", + "Archive Chat": "Sohbeti arşivle", + "Archive Group": "Grubu arşivle", + "Are you sure you want to delete this chat? This can't be undone.": "Bu sohbeti silmek istediğinize emin misiniz? Bu işlem geri alınamaz.", + "Are you sure you want to delete this group? This can't be undone.": "Bu grubu silmek istediğinize emin misiniz? Bu işlem geri alınamaz.", "Are you sure you want to permanently delete this message?": "Bu mesajı kalıcı olarak silmek istediğinizden emin misiniz?", "Are you sure?": "Emin misiniz?", "Ask a question": "Bir soru sor", @@ -21,23 +29,32 @@ "Block User": "Kullanıcıyı engelle", "Cancel": "İptal", "Cannot Flag Message": "Raporlama Başarısız", + "Change in Settings": "Ayarlar'da değiştir", + "Choose between 2–10 options": "2 ile 10 arasında seçenek seçin", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Yorumunuzun diğerlerini nasıl hissettirebileceğini düşünün ve topluluk kurallarımızı takip ettiğinizden emin olun", "Copy Message": "Mesajı Kopyala", + "Couldn't load new threads. Tap to retry": "Yeni konular yüklenemedi. Tekrar denemek için dokunun", "Create Poll": "Anket oluştur", + "Create a poll and let everyone vote": "Bir anket oluşturun ve herkesin oy vermesine izin verin", "Delete": "Sil", + "Delete Chat": "Sohbeti sil", + "Delete Group": "Grubu sil", "Delete Message": "Mesajı Sil", + "Delete chat": "Sohbeti sil", "Delete for me": "Benim için sil", + "Delete group": "Grubu sil", "Device camera is used to take photos or videos.": "Cihaz kamerası fotoğraf veya video çekmek için kullanılır.", "Device gallery permissions is used to take photos or videos.": "Cihaz galerisi izinleri fotoğraf veya video çekmek için kullanılır.", "Do you want to send a copy of this message to a moderator for further investigation?": "Detaylı inceleme için bu mesajın kopyasını moderatöre göndermek istiyor musunuz?", + "Draft": "Taslak", "Due since {{ dueSince }}": "Son tarihi {{ dueSince }} itibarıyla geçmiştir.", "Edit Message": "Mesajı Düzenle", "Edited": "Düzenlendi", "Editing Message": "Mesaj Düzenleniyor", "Emoji matching": "Emoji eşleştirme", "Empty message...": "Boş mesaj...", - "Enter a new option": "Yeni bir seçenek girin", "End Vote": "Oylamayı sonlandır", + "Enter a new option": "Yeni bir seçenek girin", "Error loading": "Yükleme hatası", "Error loading channel list...": "Kanal listesi yüklenirken hata oluştu...", "Error loading messages for this channel...": "Bu kanal için mesajlar yüklenirken hata oluştu...", @@ -55,8 +72,11 @@ "Hold to start recording.": "Kayıt yapmak için basılı tutun.", "How about sending your first message to a friend?": "İlk mesajınızı bir arkadaşınıza göndermeye ne dersiniz?", "Instant Commands": "Anlık Komutlar", + "Leave Chat": "Sohbetten ayrıl", + "Leave Group": "Gruptan ayrıl", "Let others add options": "Başkalarının seçenek eklemesine izin ver", "Let's start chatting!": "Haydi sohbete başlayalım!", + "Limit votes per person": "Kişi başına oy sayısını sınırla", "Links are disabled": "Bağlantılar devre dışı", "Live Location": "Canlı Konum", "Loading channels...": "Kanallar yükleniyor...", @@ -68,24 +88,29 @@ "Maximum number of files reached": "Maksimum dosya sayısına ulaşıldı", "Message Reactions": "Mesaj Tepkileri", "Message deleted": "Mesaj silindi", + "Message failed to send": "Mesaj gönderimi başarısız", "Message flagged": "Mesaj işaretlendi", "Multiple votes": "Çoklu oy", - "Network error": "Ağ hatası", - "Select more than one option": "Birden fazla seçenek seçin", - "Limit votes per person": "Kişi başına oy sayısını sınırla", - "Choose between 2–10 options": "2 ile 10 arasında seçenek seçin", + "Mute Group": "Grubu sessize al", "Mute User": "Kullanıcıyı sessize al", + "Network error": "Ağ hatası", "No chats here yet…": "Henüz burada sohbet yok…", + "No conversations yet": "Henüz konuşma yok", "No items exist": "Hiçbir öğe yok", + "No messages yet": "Henüz mesaj yok", "No threads here yet": "Burada henüz akış yok", "Not supported": "Desteklenmiyor", "Nothing yet...": "Henüz değil...", + "Offline": "Çevrimdışı", "Ok": "Tamam", + "Online": "Çevrimiçi", "Only visible to you": "Sadece siz görebilirsiniz", + "Open Camera": "Kamerayı aç", + "Open Files": "Dosyaları aç", "Open Settings": "Ayarları aç", "Option": "Seçenek", - "Option {{count}}": "Seçenek {{count}}", "Option already exists": "Seçenek zaten var", + "Option {{count}}": "Seçenek {{count}}", "Options": "Seçenekler", "Photo": "Fotoğraf", "Photos and Videos": "Fotoğraflar ve Videolar", @@ -97,16 +122,23 @@ "Poll Comments": "Anket Yorumları", "Poll Options": "Anket Seçenekleri", "Poll Results": "Anket Sonuçları", + "Poll has ended": "Oylama sona erdi", "Questions": "Sorular", "Reconnecting...": "Yeniden Bağlanılıyor...", + "Reminder overdue": "Hatırlatıcı süresi doldu", + "Reminder set": "Hatırlatıcı ayarlandı", + "Replied to a thread": "Konuya yanıt verildi", "Reply": "Yanıtla", - "Reply to {{name}}": "{{name}} için yanıtla", "Reply to Message": "Mesajı Yanıtla", + "Reply to a message to start a thread": "Bir mesaja yanıt vermek için konu başlatın", + "Reply to {{name}}": "{{name}} için yanıtla", "Resend": "Yeniden gönder", "Retry Upload": "Yüklemeyi yeniden dene", "SEND": "GÖNDER", "Search": "Ara", "Select More Photos": "Daha Fazla Fotoğraf Seçin", + "Select files to share": "Paylaşmak için dosyaları seç", + "Select more than one option": "Birden fazla seçenek seçin", "Select one": "Birini seç", "Select one or more": "Bir veya daha fazlasını seç", "Select up to {{count}}_many": "Seçenekleri {{count}} kadar seç", @@ -120,29 +152,61 @@ "Slow mode ON": "Yavaş Mod Açık", "Slow mode, wait {{seconds}}s...": "Yavaş mod, {{seconds}}s bekleyin...", "Suggest an option": "Bir seçenek öner", + "Take a photo and share": "Fotoğraf çek ve paylaş", + "Take a video and share": "Video çek ve paylaş", + "Tap to remove": "Kaldırmak için dokunun", "The message has been reported to a moderator.": "Mesaj moderatöre bildirildi.", "The source message was deleted": "Kaynak mesaj silindi", "Thinking...": "Düşünüyor...", "This reply was deleted": "Bu yanıt silindi", "Thread Reply": "Konu Yanıtı", "Type a number from 2 to 10": "2 ile 10 arasında bir sayı girin", + "Typing": "Yazıyor", + "Unarchive Chat": "Sohbeti arşivden çıkar", + "Unarchive Group": "Grubu arşivden çıkar", "Unban User": "Kullanıcının Yasağını Kaldır", "Unblock User": "Kullanıcının engelini kaldır", "Unknown User": "Bilinmeyen kullanıcı", + "Unmute Group": "Grubun sesini ac", "Unmute User": "Kullanıcının sesini aç", "Unpin from Conversation": "Sabitlemeyi kaldır", "Unread Messages": "Okunmamış Mesajlar", + "Unsupported Attachment": "Desteklenmeyen ek", "Update your comment": "Yorumunu güncelle", "Video": "Video", + "View": "Görüntüle", "View Results": "Sonuçları görüntüle", "View {{count}} comments_many": "Tüm {{count}} yorumları görüntüle", "View {{count}} comments_one": "{{count}} yorumu görüntüle", "View {{count}} comments_other": "{{count}} yorumu görüntüle", "Voice message": "Sesli mesaj", "Voice message ({{duration}})": "Sesli mesaj ({{duration}})", - "Your comment": "Yorumunuz", "You": "Sen", "You can't send messages in this channel": "Bu konuşmaya mesaj gönderemezsiniz", + "You have not granted access to the photo library.": "Fotoğraf kitaplığına erişim izni vermediniz.", + "You have not granted access to your camera": "Kameranıza erişim izni vermediniz", + "You voted: {{ option }}": "Oy verdiniz: {{ option }}", + "Your comment": "Yorumunuz", + "and {{ count }} others": "{{ count }} kişi daha", + "aria/AI is generating": "AI is generating", + "aria/AI is thinking": "AI is thinking", + "aria/Avatar of {{name}}": "Avatar of {{name}}", + "aria/Connected": "Connected", + "aria/Delivered": "Delivered", + "aria/Loading": "Loading", + "aria/Loading failed": "Loading failed", + "aria/New message from {{user}}": "New message from {{user}}", + "aria/Offline": "Offline", + "aria/Open message actions": "Open message actions", + "aria/Reaction {{emoji}} by {{count}} users": "Reaction {{emoji}} by {{count}} users", + "aria/Read": "Read", + "aria/Reconnecting": "Reconnecting", + "aria/Reply to {{user}}": "Reply to {{user}}", + "aria/Send message": "Send message", + "aria/Sending": "Sending", + "aria/Sent": "Sent", + "aria/Voice message recording. Hold to record.": "Voice message recording. Hold to record.", + "aria/{{count}} new messages": "{{count}} new messages", "duration/Location end at": "{{ milliseconds | durationFormatter(withSuffix: false) }}", "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", @@ -158,16 +222,11 @@ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Dün]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Yarın]\",\"nextWeek\":\"dddd [saat] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} ve {{ nonSelfUserLength }} kişi daha yazıyor", + "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} ve {{ secondUser }} yazıyor", "{{ index }} of {{ photoLength }}": "{{ index }} / {{ photoLength }}", + "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} kişi yazıyor", "{{ replyCount }} Replies": "{{ replyCount }} Cevap", "{{ user }} is typing": "{{ user }} yazıyor", - "You voted: {{ option }}": "Oy verdiniz: {{ option }}", - "{{ firstUser }} and {{ secondUser }} are typing": "{{ firstUser }} ve {{ secondUser }} yazıyor", - "{{ numberOfUsers }} people are typing": "{{ numberOfUsers }} kişi yazıyor", - "Typing": "Yazıyor", - "No messages yet": "Henüz mesaj yok", - "Message failed to send": "Mesaj gönderimi başarısız", - "and {{ count }} others": "{{ count }} kişi daha", "{{ user }} voted: {{ option }}": "{{ user }} oy verdi: {{ option }}", "{{count}} Audios_many": "{{count}} ses", "{{count}} Audios_one": "{{count}} ses", @@ -178,63 +237,23 @@ "{{count}} Photos_many": "{{count}} fotoğraf", "{{count}} Photos_one": "{{count}} fotoğraf", "{{count}} Photos_other": "{{count}} fotoğraf", - "{{count}} Voice messages_many": "{{count}} sesli mesaj", - "{{count}} Voice messages_one": "{{count}} sesli mesaj", - "{{count}} Voice messages_other": "{{count}} sesli mesaj", + "{{count}} Reactions_many": "{{count}} tepki", + "{{count}} Reactions_one": "{{count}} tepki", + "{{count}} Reactions_other": "{{count}} tepki", "{{count}} Videos_many": "{{count}} video", "{{count}} Videos_one": "{{count}} video", "{{count}} Videos_other": "{{count}} video", + "{{count}} Voice messages_many": "{{count}} sesli mesaj", + "{{count}} Voice messages_one": "{{count}} sesli mesaj", + "{{count}} Voice messages_other": "{{count}} sesli mesaj", + "{{count}} new messages": "{{count}} yeni mesaj", + "{{count}} new threads": "{{count}} yeni konu", + "{{count}} unread": "{{count}} okunmamış", "{{count}} votes_many": "{{count}} oy", "{{count}} votes_one": "{{count}} oy", "{{count}} votes_other": "{{count}} oy", - "🏙 Attachment...": "🏙 Ek...", - "You have not granted access to the photo library.": "Fotoğraf kitaplığına erişim izni vermediniz.", - "Change in Settings": "Ayarlar'da değiştir", - "Create a poll and let everyone vote": "Bir anket oluşturun ve herkesin oy vermesine izin verin", - "Open Camera": "Kamerayı aç", - "Open Files": "Dosyaları aç", - "Select files to share": "Paylaşmak için dosyaları seç", - "Take a photo and share": "Fotoğraf çek ve paylaş", - "Take a video and share": "Video çek ve paylaş", - "You have not granted access to your camera": "Kameranıza erişim izni vermediniz", - "{{count}} Reactions_many": "{{count}} tepki", - "{{count}} Reactions_one": "{{count}} tepki", - "{{count}} Reactions_other": "{{count}} tepki", - "Tap to remove": "Kaldırmak için dokunun", - "Draft": "Taslak", - "Reminder set": "Hatırlatıcı ayarlandı", - "Also sent in channel": "Kanala da gönderildi", - "Replied to a thread": "Konuya yanıt verildi", - "View": "Görüntüle", - "Reminder overdue": "Hatırlatıcı süresi doldu", - "Poll has ended": "Oylama sona erdi", - "Reply to a message to start a thread": "Bir mesaja yanıt vermek için konu başlatın", - "Couldn't load new threads. Tap to retry": "Yeni konular yüklenemedi. Tekrar denemek için dokunun", - "{{count}} new threads": "{{count}} yeni konu", - "No conversations yet": "Henüz konuşma yok", - "Are you sure you want to delete this group? This can't be undone.": "Bu grubu silmek istediğinize emin misiniz? Bu işlem geri alınamaz.", - "Are you sure you want to delete this chat? This can't be undone.": "Bu sohbeti silmek istediğinize emin misiniz? Bu işlem geri alınamaz.", - "Delete chat": "Sohbeti sil", - "Delete group": "Grubu sil", - "Archive Chat": "Sohbeti arşivle", - "Archive Group": "Grubu arşivle", - "Delete Chat": "Sohbeti sil", - "Delete Group": "Grubu sil", - "Leave Chat": "Sohbetten ayrıl", - "Leave Group": "Gruptan ayrıl", - "Mute Group": "Grubu sessize al", - "Offline": "Çevrimdışı", - "Online": "Çevrimiçi", - "Unarchive Chat": "Sohbeti arşivden çıkar", - "Unarchive Group": "Grubu arşivden çıkar", - "Unmute Group": "Grubun sesini ac", + "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} üye, {{onlineCount}} çevrimiçi", "{{memberCount}} members, {{onlineCount}} online_one": "{{memberCount}} üye, {{onlineCount}} çevrimiçi", "{{memberCount}} members, {{onlineCount}} online_other": "{{memberCount}} üye, {{onlineCount}} çevrimiçi", - "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} üye, {{onlineCount}} çevrimiçi", - "{{count}} unread": "{{count}} okunmamış", - "{{count}} new messages": "{{count}} yeni mesaj", - "Unsupported Attachment": "Desteklenmeyen ek", - "+{{count}} More Options_one": "+{{count}} seçenek daha", - "+{{count}} More Options_other": "+{{count}} seçenek daha", - "+{{count}} More Options_many": "+{{count}} seçenek daha" + "🏙 Attachment...": "🏙 Ek..." } From ad60bca9d36c2df89fba1e7e62bb5327eec8abfc Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Mon, 4 May 2026 12:19:04 +0200 Subject: [PATCH 02/19] feat(a11y): wire accessibility into base UI primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of the accessibility plan. - ui/Avatar: accepts `name` and `accessibilityLabel` props; auto-composes `aria/Avatar of {{name}}` via useA11yLabel. UserAvatar passes user.name, ChannelAvatar passes channel.data.name. Inner ImageComponent is hidden from AT so the parent View carries the label. - ui/Button: adds accessibilityRole='button' and accessibilityState ({ disabled, selected }). Existing accessibilityLabel pass-through preserved via {...rest} spread. - ui/Input: wires accessibilityLabel (defaults to title), accessibilityHint (defaults to description), accessibilityState ({ disabled, selected }). Validation errors render through a hidden assertive live region. - Indicators: LoadingIndicator wraps in role='progressbar' + accessibilityLiveRegion='polite'. LoadingDots are hidden from AT. LoadingErrorIndicator gets role='alert' (or 'button' when retryable) and an accessibilityHint. - ProgressControl: adds accessibilityRole='progressbar' (or 'adjustable' when interactive) and accessibilityValue. - UIComponents/BottomSheetModal: applies useResolvedModalAccessibilityProps (accessibilityViewIsModal on iOS, importantForAccessibility='yes' on Android) to the sheet root. No-op when accessibility.enabled is false. All changes are gated by AccessibilityContext.enabled — primitives still behave exactly as before for integrators who haven't opted in. --- package/src/a11y/__tests__/a11yUtils.test.ts | 6 ++++- .../__tests__/AccessibilityAnnouncer.test.tsx | 3 +-- .../src/components/Indicators/LoadingDots.tsx | 6 ++++- .../Indicators/LoadingErrorIndicator.tsx | 8 +++++- .../Indicators/LoadingIndicator.tsx | 2 +- .../ProgressControl/ProgressControl.tsx | 4 +++ .../UIComponents/BottomSheetModal.tsx | 4 +++ package/src/components/ui/Avatar/Avatar.tsx | 25 +++++++++++++++++-- .../components/ui/Avatar/ChannelAvatar.tsx | 3 +++ .../src/components/ui/Avatar/UserAvatar.tsx | 1 + package/src/components/ui/Button/Button.tsx | 2 ++ package/src/components/ui/Input/Input.tsx | 12 +++++++++ 12 files changed, 68 insertions(+), 8 deletions(-) diff --git a/package/src/a11y/__tests__/a11yUtils.test.ts b/package/src/a11y/__tests__/a11yUtils.test.ts index f91620f264..79a6d8ebdb 100644 --- a/package/src/a11y/__tests__/a11yUtils.test.ts +++ b/package/src/a11y/__tests__/a11yUtils.test.ts @@ -20,7 +20,11 @@ describe('composeAccessibilityLabel', () => { describe('formatAccessibilityValue', () => { it('clamps now between min and max', () => { - expect(formatAccessibilityValue({ max: 100, now: 200 })).toEqual({ max: 100, min: 0, now: 100 }); + expect(formatAccessibilityValue({ max: 100, now: 200 })).toEqual({ + max: 100, + min: 0, + now: 100, + }); expect(formatAccessibilityValue({ max: 100, min: 10, now: 5 })).toEqual({ max: 100, min: 10, diff --git a/package/src/components/Accessibility/__tests__/AccessibilityAnnouncer.test.tsx b/package/src/components/Accessibility/__tests__/AccessibilityAnnouncer.test.tsx index bb68d39ced..002300b276 100644 --- a/package/src/components/Accessibility/__tests__/AccessibilityAnnouncer.test.tsx +++ b/package/src/components/Accessibility/__tests__/AccessibilityAnnouncer.test.tsx @@ -3,11 +3,10 @@ import { AccessibilityInfo } from 'react-native'; import { renderHook, waitFor } from '@testing-library/react-native'; +import { AccessibilityProvider } from '../../../contexts/accessibilityContext/AccessibilityContext'; import { AccessibilityAnnouncer } from '../AccessibilityAnnouncer'; import { useAccessibilityAnnouncer } from '../useAccessibilityAnnouncer'; -import { AccessibilityProvider } from '../../../contexts/accessibilityContext/AccessibilityContext'; - jest.mock('react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo', () => ({ __esModule: true, default: { diff --git a/package/src/components/Indicators/LoadingDots.tsx b/package/src/components/Indicators/LoadingDots.tsx index dfcec34438..fe96766349 100644 --- a/package/src/components/Indicators/LoadingDots.tsx +++ b/package/src/components/Indicators/LoadingDots.tsx @@ -31,7 +31,11 @@ export const LoadingDots = (props: Props) => { const offsetLength = duration / numberOfDots; return ( - + {Array.from(Array(numberOfDots)).map((_item, index) => ( + {text} diff --git a/package/src/components/Indicators/LoadingIndicator.tsx b/package/src/components/Indicators/LoadingIndicator.tsx index 4f8dc88020..2e4297ca6b 100644 --- a/package/src/components/Indicators/LoadingIndicator.tsx +++ b/package/src/components/Indicators/LoadingIndicator.tsx @@ -12,7 +12,7 @@ const LoadingIndicatorWrapper = ({ text }: LoadingIndicatorWrapperProps) => { const styles = useStyles(); return ( - + {text ? ( diff --git a/package/src/components/ProgressControl/ProgressControl.tsx b/package/src/components/ProgressControl/ProgressControl.tsx index d3e7a500d2..a42d105342 100644 --- a/package/src/components/ProgressControl/ProgressControl.tsx +++ b/package/src/components/ProgressControl/ProgressControl.tsx @@ -110,9 +110,13 @@ export const ProgressControl = (props: ProgressControlProps) => { [widthInNumbers], ); + const progressPercent = Math.round(progress * 100); + return ( { setWidthInNumbers(nativeEvent.layout.width); }} diff --git a/package/src/components/UIComponents/BottomSheetModal.tsx b/package/src/components/UIComponents/BottomSheetModal.tsx index 0be2d5ed36..843aec89b8 100644 --- a/package/src/components/UIComponents/BottomSheetModal.tsx +++ b/package/src/components/UIComponents/BottomSheetModal.tsx @@ -38,6 +38,7 @@ import { getBottomSheetTopSnapIndex, } from './BottomSheetModal.utils'; +import { useResolvedModalAccessibilityProps } from '../../a11y/hooks/useResolvedModalAccessibilityProps'; import { BottomSheetProvider } from '../../contexts/bottomSheetContext/BottomSheetContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useStableCallback } from '../../hooks'; @@ -501,6 +502,8 @@ const BottomSheetModalInner = (props: PropsWithChildren) const onBackdropPress = useStableCallback(() => close()); + const modalA11yProps = useResolvedModalAccessibilityProps(); + const bottomSheetModalContextValue = useMemo( () => ({ close, @@ -523,6 +526,7 @@ const BottomSheetModalInner = (props: PropsWithChildren) diff --git a/package/src/components/ui/Avatar/Avatar.tsx b/package/src/components/ui/Avatar/Avatar.tsx index fb02eace24..6d87882fd9 100644 --- a/package/src/components/ui/Avatar/Avatar.tsx +++ b/package/src/components/ui/Avatar/Avatar.tsx @@ -3,16 +3,28 @@ import { ColorValue, StyleProp, StyleSheet, View, ViewStyle } from 'react-native import { avatarSizes } from './constants'; +import { useA11yLabel } from '../../../a11y/hooks/useA11yLabel'; import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { primitives } from '../../../theme'; export type AvatarProps = { size: '2xl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs'; + /** + * Override for the auto-generated accessibility label. When omitted and a + * `name` is provided, the SDK uses `aria/Avatar of {{name}}`. + */ + accessibilityLabel?: string; + backgroundColor?: ColorValue; imageUrl?: string; + /** + * Display name of the entity this avatar represents (user, channel). Used to + * compose the default `accessibilityLabel`. Optional — when neither this nor + * `accessibilityLabel` is provided, the avatar is left unlabeled. + */ + name?: string; placeholder?: React.ReactNode; showBorder?: boolean; - backgroundColor?: ColorValue; style?: StyleProp; }; @@ -24,14 +36,18 @@ export const Avatar = (props: AvatarProps) => { const { ImageComponent } = useComponentsContext(); const defaultAvatarBg = semantics.avatarPaletteBg1; const { + accessibilityLabel: accessibilityLabelOverride, backgroundColor = defaultAvatarBg, - size, imageUrl, + name, placeholder, showBorder, + size, style, } = props; const styles = useStyles(); + const composedLabel = useA11yLabel('aria/Avatar of {{name}}', { name: name ?? '' }); + const accessibilityLabel = accessibilityLabelOverride ?? (name ? composedLabel : undefined); const onHandleError = useCallback(() => { setError(true); @@ -39,6 +55,9 @@ export const Avatar = (props: AvatarProps) => { return ( { > {imageUrl && !error ? ( { [usersForGroup, client.user?.id], ); + const channelName = (channel.data?.name as string | undefined) ?? channel.cid; + if (channelImage) { return ( diff --git a/package/src/components/ui/Avatar/UserAvatar.tsx b/package/src/components/ui/Avatar/UserAvatar.tsx index c433395f4d..02270a5045 100644 --- a/package/src/components/ui/Avatar/UserAvatar.tsx +++ b/package/src/components/ui/Avatar/UserAvatar.tsx @@ -50,6 +50,7 @@ export const UserAvatar = (props: UserAvatarProps) => { [ { backgroundColor: pressed diff --git a/package/src/components/ui/Input/Input.tsx b/package/src/components/ui/Input/Input.tsx index 0d23a8102b..1f4e51d63f 100644 --- a/package/src/components/ui/Input/Input.tsx +++ b/package/src/components/ui/Input/Input.tsx @@ -104,6 +104,9 @@ export const Input = ({ /> ) : null} + {state === 'error' && errorMessage ? ( + + {errorMessage} + + ) : null} {RightIcon ? ( Date: Mon, 4 May 2026 12:21:39 +0200 Subject: [PATCH 03/19] feat(a11y): add accessible message actions, reactions, and reply preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3a of the accessibility plan. - MessageMenu/MessageActionList: ScrollView gets accessibilityRole='menu' and a t('aria/Message actions') label via useA11yLabel. - MessageMenu/MessageActionListItem: Pressable gets accessibilityRole= 'menuitem' and the action's title as label. Inner View hidden from AT so the menuitem speaks once. - MessageMenu/ReactionButton: label switches to t('aria/Reaction {{emoji}} by {{count}} users') via useA11yLabel; falls back to the legacy hardcoded testID-style label when accessibility is disabled. - MessageMenu/MessageReactionPicker: outer View gets accessibilityRole= 'menu'. - Reply/Reply: outer View gets accessibilityRole='text' + the composed reply title as accessibilityLabel. - aria/Message actions key added to all 12 locales. Message.tsx untouched in this commit — the long-press alternative button and composed message label are deferred to a follow-up since they require careful placement inside MessageItemView. --- .../src/components/MessageMenu/MessageActionList.tsx | 5 ++++- .../components/MessageMenu/MessageActionListItem.tsx | 6 +++++- .../components/MessageMenu/MessageReactionPicker.tsx | 1 + package/src/components/MessageMenu/ReactionButton.tsx | 10 +++++++++- package/src/components/Reply/Reply.tsx | 6 +++++- package/src/i18n/en.json | 1 + package/src/i18n/es.json | 1 + package/src/i18n/fr.json | 1 + package/src/i18n/he.json | 1 + package/src/i18n/hi.json | 1 + package/src/i18n/it.json | 1 + package/src/i18n/ja.json | 1 + package/src/i18n/ko.json | 1 + package/src/i18n/nl.json | 1 + package/src/i18n/pt-br.json | 1 + package/src/i18n/ru.json | 1 + package/src/i18n/tr.json | 1 + 17 files changed, 36 insertions(+), 4 deletions(-) diff --git a/package/src/components/MessageMenu/MessageActionList.tsx b/package/src/components/MessageMenu/MessageActionList.tsx index a37bac7a3c..1f42cfb958 100644 --- a/package/src/components/MessageMenu/MessageActionList.tsx +++ b/package/src/components/MessageMenu/MessageActionList.tsx @@ -5,6 +5,7 @@ import { ScrollView } from 'react-native-gesture-handler'; import { MessageActionType } from './MessageActionListItem'; +import { useA11yLabel } from '../../a11y/hooks/useA11yLabel'; import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { primitives } from '../../theme'; @@ -24,6 +25,7 @@ export type MessageActionListProps = { export const MessageActionList = (props: MessageActionListProps) => { const { messageActions } = props; const { MessageActionListItem } = useComponentsContext(); + const a11yLabel = useA11yLabel('aria/Message actions'); const { theme: { messageMenu: { @@ -43,7 +45,8 @@ export const MessageActionList = (props: MessageActionListProps) => { return ( diff --git a/package/src/components/MessageMenu/MessageActionListItem.tsx b/package/src/components/MessageMenu/MessageActionListItem.tsx index 91e711d132..f7ebe64d10 100644 --- a/package/src/components/MessageMenu/MessageActionListItem.tsx +++ b/package/src/components/MessageMenu/MessageActionListItem.tsx @@ -77,6 +77,8 @@ export const MessageActionListItem = (props: MessageActionListItemProps) => { return ( [ styles.buttonContainer, @@ -84,8 +86,10 @@ export const MessageActionListItem = (props: MessageActionListItemProps) => { ]} > {icon} {title} diff --git a/package/src/components/MessageMenu/MessageReactionPicker.tsx b/package/src/components/MessageMenu/MessageReactionPicker.tsx index 3b9905e42e..02a950209b 100644 --- a/package/src/components/MessageMenu/MessageReactionPicker.tsx +++ b/package/src/components/MessageMenu/MessageReactionPicker.tsx @@ -163,6 +163,7 @@ export const MessageReactionPicker = (props: MessageReactionPickerProps) => { return ( diff --git a/package/src/components/MessageMenu/ReactionButton.tsx b/package/src/components/MessageMenu/ReactionButton.tsx index 4fe4ac39bd..fc5c7962f0 100644 --- a/package/src/components/MessageMenu/ReactionButton.tsx +++ b/package/src/components/MessageMenu/ReactionButton.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useMemo } from 'react'; import { StyleSheet, View } from 'react-native'; +import { useA11yLabel } from '../../a11y/hooks/useA11yLabel'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { IconProps } from '../../icons'; import { Button, ButtonProps } from '../ui'; @@ -50,10 +51,17 @@ export const ReactionButton = (props: ReactionButtonProps) => { [Icon, reactionIconSize], ); + const a11yLabel = useA11yLabel('aria/Reaction {{emoji}} by {{count}} users', { + count: count ?? '0', + emoji: type, + }); + return (