From 1b3eb342feb0552089e301c17a9a06b9ef137321 Mon Sep 17 00:00:00 2001 From: "Situ Chandra Shil (via MelvinBot)" Date: Wed, 11 Mar 2026 20:58:20 +0000 Subject: [PATCH 1/8] Fix: Announce dynamic content for screen readers (search suggestions + timer countdown) Add accessibility live regions and announcements for two categories of dynamic content: 1. Search suggestions: Add visually-hidden live region text in BaseSelectionList that announces result count (debounced) when filtering changes. Add accessibilityLiveRegion on BaseAutoCompleteSuggestions container with count text. 2. Timer countdown: Add AccessibilityInfo.announceForAccessibility calls in ValidateCodeCountdown at countdown start/reset and expiration (not every second). Wrap timer output in View with accessibilityLiveRegion="polite" for web/Android. Addresses WCAG 4.1.3 (Status Messages). Co-authored-by: Situ Chandra Shil --- .../BaseAutoCompleteSuggestions.tsx | 5 +++ .../SelectionList/BaseSelectionList.tsx | 28 +++++++++++++++++ .../ValidateCodeCountdown/index.tsx | 31 ++++++++++++++++--- src/languages/en.ts | 4 +++ src/languages/es.ts | 4 +++ 5 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 0cd97ef55eb6..3b746904b18a 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -4,6 +4,8 @@ import {FlatList} from 'react-native-gesture-handler'; import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {hasHoverSupport} from '@libs/DeviceCapabilities'; @@ -23,6 +25,7 @@ function BaseAutoCompleteSuggestions({ measuredHeightOfSuggestionRows, }: ExternalProps) { const styles = useThemeStyles(); + const {translate} = useLocalize(); const StyleUtils = useStyleUtils(); const rowHeight = useSharedValue(0); const prevRowHeightRef = useRef(measuredHeightOfSuggestionRows); @@ -92,6 +95,7 @@ function BaseAutoCompleteSuggestions({ return ( { if (hasHoverSupport()) { return; @@ -99,6 +103,7 @@ function BaseAutoCompleteSuggestions({ e.preventDefault(); }} > + {translate('common.suggestionsAvailable', suggestions.length)} ({ setShouldDisableHoverStyle = () => {}, }: SelectionListProps) { const styles = useThemeStyles(); + const {translate} = useLocalize(); const isFocused = useIsFocused(); const scrollEnabled = useScrollEnabled(); const {singleExecution} = useSingleExecution(); @@ -110,6 +113,23 @@ function BaseSelectionList({ const initialFocusedIndex = useMemo(() => data.findIndex((i) => i.keyForList === initiallyFocusedItemKey), [data, initiallyFocusedItemKey]); const [itemsToHighlight, setItemsToHighlight] = useState | null>(null); + const [announcedResultCount, setAnnouncedResultCount] = useState(''); + const announceTimeoutRef = useRef(null); + + // Debounced announcement of result count for screen readers + useEffect(() => { + if (!shouldShowTextInput) { + return; + } + + announceTimeoutRef.current = setTimeout(() => { + setAnnouncedResultCount(translate('common.resultsAvailable', data.length)); + }, CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME); + + return () => { + clearTimeout(announceTimeoutRef.current ?? undefined); + }; + }, [data.length, shouldShowTextInput, translate]); const isItemSelected = useCallback( (item: TItem) => item.isSelected ?? ((isSelected?.(item) ?? selectedItems.includes(item.keyForList)) && canSelectMultiple), @@ -544,6 +564,14 @@ function BaseSelectionList({ return ( {textInputComponent({shouldBeInsideList: false})} + {shouldShowTextInput && ( + + {announcedResultCount} + + )} {data.length === 0 && (shouldShowLoadingPlaceholder || shouldShowListEmptyContent) ? ( renderListEmptyContent() ) : ( diff --git a/src/components/ValidateCodeCountdown/index.tsx b/src/components/ValidateCodeCountdown/index.tsx index ea0435ac9c66..b71ab0b40817 100644 --- a/src/components/ValidateCodeCountdown/index.tsx +++ b/src/components/ValidateCodeCountdown/index.tsx @@ -1,4 +1,5 @@ import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {AccessibilityInfo, View} from 'react-native'; import RenderHTML from '@components/RenderHTML'; import useLocalize from '@hooks/useLocalize'; import CONST from '@src/CONST'; @@ -9,6 +10,7 @@ function ValidateCodeCountdown({onCountdownFinish, ref}: ValidateCodeCountdownPr const [timeRemaining, setTimeRemaining] = useState(CONST.REQUEST_CODE_DELAY); const timerRef = useRef(undefined); + const previousTimeRemainingRef = useRef(CONST.REQUEST_CODE_DELAY); useImperativeHandle(ref, () => ({ resetCountdown: () => setTimeRemaining(CONST.REQUEST_CODE_DELAY), @@ -27,12 +29,31 @@ function ValidateCodeCountdown({onCountdownFinish, ref}: ValidateCodeCountdownPr }; }, [onCountdownFinish, timeRemaining]); + // Announce countdown start/reset and expiration for screen readers (iOS) + useEffect(() => { + if (timeRemaining === CONST.REQUEST_CODE_DELAY && previousTimeRemainingRef.current !== CONST.REQUEST_CODE_DELAY) { + // Countdown was reset + AccessibilityInfo.announceForAccessibility(translate('validateCodeForm.timeRemainingAnnouncement', {timeRemaining: CONST.REQUEST_CODE_DELAY})); + } else if (timeRemaining === 0) { + AccessibilityInfo.announceForAccessibility(translate('validateCodeForm.timeExpiredAnnouncement')); + } + previousTimeRemainingRef.current = timeRemaining; + }, [timeRemaining, translate]); + + // Announce on initial mount + useEffect(() => { + AccessibilityInfo.announceForAccessibility(translate('validateCodeForm.timeRemainingAnnouncement', {timeRemaining: CONST.REQUEST_CODE_DELAY})); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( - + + + ); } diff --git a/src/languages/en.ts b/src/languages/en.ts index fc93250f63a2..ab67a7481dfe 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -281,6 +281,8 @@ const translations = { na: 'N/A', noResultsFound: 'No results found', noResultsFoundMatching: (searchString: string) => `No results found matching "${searchString}"`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'result' : 'results'} available`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'suggestion' : 'suggestions'} available`, recentDestinations: 'Recent destinations', timePrefix: "It's", conjunctionFor: 'for', @@ -2712,6 +2714,8 @@ const translations = { requiredWhen2FAEnabled: 'Required when 2FA is enabled', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Request a new code in ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Request a new code', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Time remaining: ${timeRemaining} seconds`, + timeExpiredAnnouncement: 'The time has expired', error: { pleaseFillMagicCode: 'Please enter your magic code', incorrectMagicCode: 'Incorrect or invalid magic code. Please try again or request a new code.', diff --git a/src/languages/es.ts b/src/languages/es.ts index be581f106c5b..ec6cadbac4bf 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -151,6 +151,8 @@ const translations: TranslationDeepObject = { na: 'N/A', noResultsFound: 'No se han encontrado resultados', noResultsFoundMatching: (searchString: string) => `No se encontraron resultados que coincidan con "${searchString}"`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'resultado disponible' : 'resultados disponibles'}`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'sugerencia disponible' : 'sugerencias disponibles'}`, recentDestinations: 'Destinos recientes', timePrefix: 'Son las', conjunctionFor: 'para', @@ -2540,6 +2542,8 @@ ${amount} para ${merchant} - ${date}`, requiredWhen2FAEnabled: 'Obligatorio cuando A2F está habilitado', requestNewCode: ({timeRemaining}) => `Pedir un código nuevo en ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Solicitar un nuevo código', + timeRemainingAnnouncement: ({timeRemaining}) => `Tiempo restante: ${timeRemaining} segundos`, + timeExpiredAnnouncement: 'El tiempo ha expirado', error: { pleaseFillMagicCode: 'Por favor, introduce el código mágico.', incorrectMagicCode: 'Código mágico incorrecto o no válido. Inténtalo de nuevo o solicita otro código.', From 9e62cb19ba67d4e0eb874b9244b5ced0da734823 Mon Sep 17 00:00:00 2001 From: "Situ Chandra Shil (via MelvinBot)" Date: Wed, 11 Mar 2026 21:03:23 +0000 Subject: [PATCH 2/8] Fix ESLint and Prettier check failures Add missing sentryLabel prop to PressableWithFeedback in BaseAutoCompleteSuggestions and fix import ordering in BaseSelectionList to satisfy Prettier. Co-authored-by: Situ Chandra Shil --- .../AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx | 1 + src/components/SelectionList/BaseSelectionList.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 3b746904b18a..008a687a0efe 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -44,6 +44,7 @@ function BaseAutoCompleteSuggestions({ onLongPress={() => {}} accessibilityLabel={accessibilityLabelExtractor(item, index)} role={CONST.ROLE.MENUITEM} + sentryLabel="AutoCompleteSuggestion-Item" > {renderSuggestionMenuItem(item, index)} diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 37686feffa57..77111fdf3867 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -5,15 +5,15 @@ import {deepEqual} from 'fast-equals'; import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {TextInputKeyPressEvent} from 'react-native'; import {Keyboard, View} from 'react-native'; -import Text from '@components/Text'; import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; +import Text from '@components/Text'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useActiveElementRole from '@hooks/useActiveElementRole'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useDebounce from '@hooks/useDebounce'; -import useLocalize from '@hooks/useLocalize'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useKeyboardState from '@hooks/useKeyboardState'; +import useLocalize from '@hooks/useLocalize'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useScrollEnabled from '@hooks/useScrollEnabled'; import useSingleExecution from '@hooks/useSingleExecution'; From 071a2f01a18ac4b6ded0bd48f94450a48519d96a Mon Sep 17 00:00:00 2001 From: "Situ Chandra Shil (via MelvinBot)" Date: Wed, 11 Mar 2026 21:10:00 +0000 Subject: [PATCH 3/8] Fix: Add missing translation keys to all language files Add resultsAvailable, suggestionsAvailable, timeRemainingAnnouncement, and timeExpiredAnnouncement to de, fr, it, ja, nl, pl, pt-BR, and zh-hans language files to fix TypeScript check failures. Co-authored-by: Situ Chandra Shil --- src/languages/de.ts | 6 +++++- src/languages/fr.ts | 4 ++++ src/languages/it.ts | 4 ++++ src/languages/ja.ts | 4 ++++ src/languages/nl.ts | 4 ++++ src/languages/pl.ts | 4 ++++ src/languages/pt-BR.ts | 4 ++++ src/languages/zh-hans.ts | 6 +++++- 8 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 4af8f69631dc..1d1c1aa1114e 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -277,7 +277,9 @@ const translations: TranslationDeepObject = { send: 'Senden', na: 'k. A.', noResultsFound: 'Keine Ergebnisse gefunden', - noResultsFoundMatching: (searchString: string) => `Keine Ergebnisse gefunden für „${searchString}“`, + noResultsFoundMatching: (searchString: string) => `Keine Ergebnisse gefunden für „${searchString}”`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'Ergebnis verfügbar' : 'Ergebnisse verfügbar'}`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'Vorschlag verfügbar' : 'Vorschläge verfügbar'}`, recentDestinations: 'Letzte Ziele', timePrefix: 'Es ist', conjunctionFor: 'für', @@ -2683,6 +2685,8 @@ ${amount} für ${merchant} – ${date}`, requiredWhen2FAEnabled: 'Erforderlich, wenn 2FA aktiviert ist', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Fordere einen neuen Code an in ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Neuen Code anfordern', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Verbleibende Zeit: ${timeRemaining} Sekunden`, + timeExpiredAnnouncement: 'Die Zeit ist abgelaufen', error: { pleaseFillMagicCode: 'Bitte gib deinen Magic Code ein', incorrectMagicCode: 'Falscher oder ungültiger Magic-Code. Bitte versuche es erneut oder fordere einen neuen Code an.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 693385c20d0b..23b09af1b76b 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -278,6 +278,8 @@ const translations: TranslationDeepObject = { na: 'N/D', noResultsFound: 'Aucun résultat trouvé', noResultsFoundMatching: (searchString: string) => `Aucun résultat trouvé correspondant à « ${searchString} »`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'résultat disponible' : 'résultats disponibles'}`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'suggestion disponible' : 'suggestions disponibles'}`, recentDestinations: 'Destinations récentes', timePrefix: "C'est", conjunctionFor: 'pour', @@ -2688,6 +2690,8 @@ ${amount} pour ${merchant} - ${date}`, requiredWhen2FAEnabled: 'Obligatoire lorsque l’authentification à deux facteurs est activée', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Demander un nouveau code dans ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Demander un nouveau code', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Temps restant : ${timeRemaining} secondes`, + timeExpiredAnnouncement: 'Le temps est écoulé', error: { pleaseFillMagicCode: 'Veuillez saisir votre code magique', incorrectMagicCode: 'Code magique incorrect ou non valide. Veuillez réessayer ou demander un nouveau code.', diff --git a/src/languages/it.ts b/src/languages/it.ts index 03e319663de8..1a5ab34a89af 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -278,6 +278,8 @@ const translations: TranslationDeepObject = { na: 'N/D', noResultsFound: 'Nessun risultato trovato', noResultsFoundMatching: (searchString: string) => `Nessun risultato trovato per "${searchString}"`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'risultato disponibile' : 'risultati disponibili'}`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'suggerimento disponibile' : 'suggerimenti disponibili'}`, recentDestinations: 'Destinazioni recenti', timePrefix: 'È', conjunctionFor: 'per', @@ -2677,6 +2679,8 @@ ${amount} per ${merchant} - ${date}`, requiredWhen2FAEnabled: 'Obbligatorio quando l’autenticazione a due fattori è abilitata', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Richiedi un nuovo codice tra ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Richiedi un nuovo codice', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Tempo rimanente: ${timeRemaining} secondi`, + timeExpiredAnnouncement: 'Il tempo è scaduto', error: { pleaseFillMagicCode: 'Inserisci il tuo codice magico', incorrectMagicCode: 'Codice magico errato o non valido. Riprova o richiedi un nuovo codice.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 3558d5415567..4688aea13d47 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -278,6 +278,8 @@ const translations: TranslationDeepObject = { na: '該当なし', noResultsFound: '結果が見つかりません', noResultsFoundMatching: (searchString: string) => `"${searchString}" に一致する結果は見つかりませんでした`, + resultsAvailable: (count: number) => `${count}件の結果が利用可能`, + suggestionsAvailable: (count: number) => `${count}件の候補が利用可能`, recentDestinations: '最近の宛先', timePrefix: 'それは', conjunctionFor: '〜用', @@ -2657,6 +2659,8 @@ ${date} の ${merchant} への ${amount}`, requiredWhen2FAEnabled: '2要素認証が有効な場合は必須', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `${timeRemaining} 後に新しいコードをリクエストする`, requestNewCodeAfterErrorOccurred: '新しいコードをリクエスト', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `残り時間: ${timeRemaining}秒`, + timeExpiredAnnouncement: '時間切れです', error: { pleaseFillMagicCode: 'マジックコードを入力してください', incorrectMagicCode: '魔法コードが間違っているか無効です。もう一度お試しいただくか、新しいコードをリクエストしてください。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index e0abe0a1bd3f..2c59107c6a37 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -278,6 +278,8 @@ const translations: TranslationDeepObject = { na: 'n.v.t.', noResultsFound: 'Geen resultaten gevonden', noResultsFoundMatching: (searchString: string) => `Geen resultaten gevonden voor "${searchString}"`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'resultaat beschikbaar' : 'resultaten beschikbaar'}`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'suggestie beschikbaar' : 'suggesties beschikbaar'}`, recentDestinations: 'Recente bestemmingen', timePrefix: 'Het is', conjunctionFor: 'voor', @@ -2676,6 +2678,8 @@ ${amount} voor ${merchant} - ${date}`, requiredWhen2FAEnabled: 'Vereist wanneer 2FA is ingeschakeld', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Vraag een nieuwe code aan over ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Een nieuwe code aanvragen', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Resterende tijd: ${timeRemaining} seconden`, + timeExpiredAnnouncement: 'De tijd is verstreken', error: { pleaseFillMagicCode: 'Voer je magische code in', incorrectMagicCode: 'Onjuiste of ongeldige magische code. Probeer het opnieuw of vraag een nieuwe code aan.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 01d78ca9342f..f9e01f380424 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -278,6 +278,8 @@ const translations: TranslationDeepObject = { na: 'ND dotyczy', noResultsFound: 'Nie znaleziono wyników', noResultsFoundMatching: (searchString: string) => `Nie znaleziono wyników pasujących do „${searchString}”`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'wynik dostępny' : 'wyników dostępnych'}`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'sugestia dostępna' : 'sugestii dostępnych'}`, recentDestinations: 'Ostatnie miejsca docelowe', timePrefix: 'To jest', conjunctionFor: 'dla', @@ -2669,6 +2671,8 @@ ${amount} dla ${merchant} - ${date}`, requiredWhen2FAEnabled: 'Wymagane, gdy włączone jest 2FA', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Poproś o nowy kod za ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Poproś o nowy kod', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Pozostały czas: ${timeRemaining} sekund`, + timeExpiredAnnouncement: 'Czas minął', error: { pleaseFillMagicCode: 'Wprowadź swój magiczny kod', incorrectMagicCode: 'Nieprawidłowy lub niepoprawny kod magiczny. Spróbuj ponownie lub poproś o nowy kod.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index c7ffffd482f4..8048b595b037 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -278,6 +278,8 @@ const translations: TranslationDeepObject = { na: 'N/D', noResultsFound: 'Nenhum resultado encontrado', noResultsFoundMatching: (searchString: string) => `Nenhum resultado encontrado para "${searchString}"`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'resultado disponível' : 'resultados disponíveis'}`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'sugestão disponível' : 'sugestões disponíveis'}`, recentDestinations: 'Destinos recentes', timePrefix: 'É', conjunctionFor: 'para', @@ -2669,6 +2671,8 @@ ${amount} para ${merchant} - ${date}`, requiredWhen2FAEnabled: 'Obrigatório quando a 2FA estiver ativada', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Solicite um novo código em ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Solicitar um novo código', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Tempo restante: ${timeRemaining} segundos`, + timeExpiredAnnouncement: 'O tempo expirou', error: { pleaseFillMagicCode: 'Insira seu código mágico', incorrectMagicCode: 'Código mágico incorreto ou inválido. Tente novamente ou solicite um novo código.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index d9a523c8b8b2..0d97b37573d9 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -277,7 +277,9 @@ const translations: TranslationDeepObject = { send: '发送', na: '不适用', noResultsFound: '未找到结果', - noResultsFoundMatching: (searchString: string) => `未找到与“${searchString}”匹配的结果`, + noResultsFoundMatching: (searchString: string) => `未找到与”${searchString}”匹配的结果`, + resultsAvailable: (count: number) => `${count}个结果可用`, + suggestionsAvailable: (count: number) => `${count}个建议可用`, recentDestinations: '最近目的地', timePrefix: '它是', conjunctionFor: '用于', @@ -2617,6 +2619,8 @@ ${amount},商户:${merchant} - 日期:${date}`, requiredWhen2FAEnabled: '启用双重验证时必填', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `在${timeRemaining}后请求新代码`, requestNewCodeAfterErrorOccurred: '请求新验证码', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `剩余时间: ${timeRemaining}秒`, + timeExpiredAnnouncement: '时间已过期', error: { pleaseFillMagicCode: '请输入你的魔法验证码', incorrectMagicCode: '魔术验证码不正确或无效。请重试或请求新的验证码。', From 036f0684481818284f15d5fe1584c8374fd85046 Mon Sep 17 00:00:00 2001 From: "Situ Chandra Shil (via MelvinBot)" Date: Wed, 11 Mar 2026 21:25:27 +0000 Subject: [PATCH 4/8] Fix: Add missing Onyx connection for allReportsViolations in ReportUtils The hasVisibleReportFieldViolations function referenced allReportsViolations but it was never declared or connected to Onyx, causing a TypeScript error. Added the module-level Onyx.connect for REPORT_VIOLATIONS collection, following the same pattern as other collection connections in the file. Co-authored-by: Situ Chandra Shil --- src/libs/ReportUtils.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8713e0ef84c6..5d289982b979 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1095,6 +1095,13 @@ Onyx.connect({ callback: (value) => (allPolicyDrafts = value), }); +let allReportsViolations: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_VIOLATIONS, + waitForCollectionCallback: true, + callback: (value) => (allReportsViolations = value), +}); + let allReports: OnyxCollection; let reportsByPolicyID: ReportByPolicyMap; Onyx.connect({ From 43b9c5d436a4bf7af92a9e3dd8ad55f6a3daa9a4 Mon Sep 17 00:00:00 2001 From: "Situ Chandra Shil (via MelvinBot)" Date: Wed, 11 Mar 2026 21:40:40 +0000 Subject: [PATCH 5/8] Fix: Revert to parameter-based reportViolations in hasVisibleReportFieldViolations The module-level Onyx.connect approach for allReportsViolations was triggering a CI-specific ESLint no-unused-vars false positive. Reverting to the original pattern where reportViolations is passed as a parameter from reportAttributes.ts, which matches the main branch approach and avoids the lint error. Co-authored-by: Situ Chandra Shil --- src/libs/ReportUtils.ts | 10 +--------- .../actions/OnyxDerived/configs/reportAttributes.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5d289982b979..f8d544f17bef 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1095,13 +1095,6 @@ Onyx.connect({ callback: (value) => (allPolicyDrafts = value), }); -let allReportsViolations: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_VIOLATIONS, - waitForCollectionCallback: true, - callback: (value) => (allReportsViolations = value), -}); - let allReports: OnyxCollection; let reportsByPolicyID: ReportByPolicyMap; Onyx.connect({ @@ -13071,7 +13064,7 @@ function getReportFieldMaps(report: OnyxEntry, fieldList: Record, policy: OnyxEntry): boolean { +function hasVisibleReportFieldViolations(report: OnyxEntry, policy: OnyxEntry, reportViolations?: OnyxEntry): boolean { if (!report || !policy?.fieldList || !policy?.areReportFieldsEnabled) { return false; } @@ -13080,7 +13073,6 @@ function hasVisibleReportFieldViolations(report: OnyxEntry, policy: Onyx return false; } - const reportViolations = allReportsViolations?.[`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report.reportID}`]; const {fieldsByName} = getReportFieldMaps(report, policy.fieldList); return Object.values(fieldsByName).some((field) => { diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index 337a8cdd753b..899d8c518781 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -19,7 +19,8 @@ const prepareReportKeys = (keys: string[]) => { key .replace(ONYXKEYS.COLLECTION.REPORT_METADATA, ONYXKEYS.COLLECTION.REPORT) .replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ONYXKEYS.COLLECTION.REPORT) - .replace(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, ONYXKEYS.COLLECTION.REPORT), + .replace(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, ONYXKEYS.COLLECTION.REPORT) + .replace(ONYXKEYS.COLLECTION.REPORT_VIOLATIONS, ONYXKEYS.COLLECTION.REPORT), ), ), ]; @@ -71,10 +72,11 @@ export default createOnyxDerivedValueConfig({ ONYXKEYS.SESSION, ONYXKEYS.COLLECTION.POLICY, ONYXKEYS.COLLECTION.POLICY_TAGS, + ONYXKEYS.COLLECTION.REPORT_VIOLATIONS, ONYXKEYS.COLLECTION.REPORT_METADATA, ], compute: ( - [reports, preferredLocale, transactionViolations, reportActions, reportNameValuePairs, transactions, personalDetails, session, policies, policyTags], + [reports, preferredLocale, transactionViolations, reportActions, reportNameValuePairs, transactions, personalDetails, session, policies, policyTags, reportViolations], {currentValue, sourceValues}, ) => { // Check if display names changed when personal details are updated @@ -105,6 +107,7 @@ export default createOnyxDerivedValueConfig({ const reportMetadataUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_METADATA] ?? {}; const reportActionsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_ACTIONS] ?? {}; const reportNameValuePairsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS] ?? {}; + const reportViolationsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_VIOLATIONS] ?? {}; const transactionsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.TRANSACTION]; const transactionViolationsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]; let dataToIterate = Object.keys(reports); @@ -129,6 +132,7 @@ export default createOnyxDerivedValueConfig({ ...Object.keys(reportMetadataUpdates), ...Object.keys(reportActionsUpdates), ...Object.keys(reportNameValuePairsUpdates), + ...Object.keys(reportViolationsUpdates), ...Array.from(reportUpdatesRelatedToReportActions), ]; @@ -203,7 +207,7 @@ export default createOnyxDerivedValueConfig({ }); const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; - const hasFieldViolations = hasVisibleReportFieldViolations(report, policy); + const hasFieldViolations = hasVisibleReportFieldViolations(report, policy, reportViolations?.[`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report.reportID}`]); let brickRoadStatus; // if report has errors or violations, show red dot From 813a019cf95af1107bd532ff38a1411db3ac81f0 Mon Sep 17 00:00:00 2001 From: "Situ Chandra Shil (via MelvinBot)" Date: Mon, 16 Mar 2026 14:22:45 +0000 Subject: [PATCH 6/8] Fix: run prettier on reportAttributes.ts Co-authored-by: Situ Chandra Shil --- .../actions/OnyxDerived/configs/reportAttributes.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index c8b89b66be5c..1cf8d7b402f9 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -227,7 +227,18 @@ export default createOnyxDerivedValueConfig({ let brickRoadStatus; let actionBadge; // if report has errors or violations, show red dot - if (SidebarUtils.shouldShowRedBrickRoad(report, chatReport, reportActionsList, hasAnyViolations || hasFieldViolations, reportErrors, transactions, transactionViolations, !!isReportArchived)) { + if ( + SidebarUtils.shouldShowRedBrickRoad( + report, + chatReport, + reportActionsList, + hasAnyViolations || hasFieldViolations, + reportErrors, + transactions, + transactionViolations, + !!isReportArchived, + ) + ) { brickRoadStatus = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; actionBadge = CONST.REPORT.ACTION_BADGE.FIX; } From 6489538f4a0f2bf636a7fc7f1ce1a1df7cb9a901 Mon Sep 17 00:00:00 2001 From: "Situ Chandra Shil (via MelvinBot)" Date: Mon, 16 Mar 2026 14:34:24 +0000 Subject: [PATCH 7/8] Fix: update OnyxDerivedTest to match new 12-element dependency array The reportAttributes config added REPORT_VIOLATIONS as a dependency, increasing the array from 11 to 12 elements. The test's compute() calls need the matching element count. Co-authored-by: Situ Chandra Shil --- tests/unit/OnyxDerivedTest.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/OnyxDerivedTest.tsx b/tests/unit/OnyxDerivedTest.tsx index d12448d36d7c..028ee038fd3b 100644 --- a/tests/unit/OnyxDerivedTest.tsx +++ b/tests/unit/OnyxDerivedTest.tsx @@ -123,9 +123,9 @@ describe('OnyxDerived', () => { const transaction = createRandomTransaction(1); // When the report attributes are recomputed with both report and transaction updates - reportAttributes.compute([reports, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined], {}); + reportAttributes.compute([reports, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined], {}); const reportAttributesComputedValue = reportAttributes.compute( - [reports, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined], + [reports, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined], { sourceValues: { [ONYXKEYS.COLLECTION.REPORT]: { From acc0fd3bf1d9e243260717cdee5a08455817c422 Mon Sep 17 00:00:00 2001 From: "Situ Chandra Shil (via MelvinBot)" Date: Tue, 17 Mar 2026 14:41:51 +0000 Subject: [PATCH 8/8] Fix: Use useAccessibilityAnnouncement hook in ValidateCodeCountdown Co-authored-by: Situ Chandra Shil --- .../ValidateCodeCountdown/index.tsx | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/components/ValidateCodeCountdown/index.tsx b/src/components/ValidateCodeCountdown/index.tsx index b71ab0b40817..7dbf49faaa4e 100644 --- a/src/components/ValidateCodeCountdown/index.tsx +++ b/src/components/ValidateCodeCountdown/index.tsx @@ -1,6 +1,7 @@ import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {AccessibilityInfo, View} from 'react-native'; +import {View} from 'react-native'; import RenderHTML from '@components/RenderHTML'; +import useAccessibilityAnnouncement from '@hooks/useAccessibilityAnnouncement'; import useLocalize from '@hooks/useLocalize'; import CONST from '@src/CONST'; import type {ValidateCodeCountdownProps} from './types'; @@ -10,7 +11,6 @@ function ValidateCodeCountdown({onCountdownFinish, ref}: ValidateCodeCountdownPr const [timeRemaining, setTimeRemaining] = useState(CONST.REQUEST_CODE_DELAY); const timerRef = useRef(undefined); - const previousTimeRemainingRef = useRef(CONST.REQUEST_CODE_DELAY); useImperativeHandle(ref, () => ({ resetCountdown: () => setTimeRemaining(CONST.REQUEST_CODE_DELAY), @@ -29,22 +29,11 @@ function ValidateCodeCountdown({onCountdownFinish, ref}: ValidateCodeCountdownPr }; }, [onCountdownFinish, timeRemaining]); - // Announce countdown start/reset and expiration for screen readers (iOS) - useEffect(() => { - if (timeRemaining === CONST.REQUEST_CODE_DELAY && previousTimeRemainingRef.current !== CONST.REQUEST_CODE_DELAY) { - // Countdown was reset - AccessibilityInfo.announceForAccessibility(translate('validateCodeForm.timeRemainingAnnouncement', {timeRemaining: CONST.REQUEST_CODE_DELAY})); - } else if (timeRemaining === 0) { - AccessibilityInfo.announceForAccessibility(translate('validateCodeForm.timeExpiredAnnouncement')); - } - previousTimeRemainingRef.current = timeRemaining; - }, [timeRemaining, translate]); + // Announce countdown start/reset for screen readers + useAccessibilityAnnouncement(translate('validateCodeForm.timeRemainingAnnouncement', {timeRemaining: CONST.REQUEST_CODE_DELAY}), timeRemaining === CONST.REQUEST_CODE_DELAY); - // Announce on initial mount - useEffect(() => { - AccessibilityInfo.announceForAccessibility(translate('validateCodeForm.timeRemainingAnnouncement', {timeRemaining: CONST.REQUEST_CODE_DELAY})); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // Announce expiration for screen readers + useAccessibilityAnnouncement(translate('validateCodeForm.timeExpiredAnnouncement'), timeRemaining === 0); return (