diff --git a/locales/rn/en.json b/locales/rn/en.json index f32f685c..a0e73917 100644 --- a/locales/rn/en.json +++ b/locales/rn/en.json @@ -56,6 +56,10 @@ "response_sent": "Response sent", "tentative": "Maybe", "title": "Calendar invitation", + "trust_authentication_failed": "This message failed sender authentication (SPF/DKIM/DMARC).", + "trust_authentication_missing": "The sender could not be verified.", + "trust_sender_mismatch": "The sender differs from the event organizer.", + "trust_sender_mismatch_unverified": "The sender does not match the organizer and the message is not authenticated.", "trust_verified": "Sender verified" }, "management": { @@ -266,6 +270,15 @@ "emails_archived_count": "{count, plural, one {# email} other {# emails}} archived", "emails_moved_to": "{count, plural, one {# email} other {# emails}} moved to {mailbox}", "error": "Error", + "filter_chip": "{field}: {value}", + "filter_title": "Filter emails", + "folder_counts": "{total} total · {unread} unread", + "import": { + "none": "No messages were imported.", + "partial": "{imported} imported, {failed} failed.", + "success": "{count, plural, one {# message imported.} other {# messages imported.}}", + "title": "Import" + }, "marked_as_spam": "Marked as spam", "marked_as_spam_count": "{count, plural, one {# email} other {# emails}} marked as spam", "marked_not_spam": "Marked as not spam", @@ -285,10 +298,12 @@ "outbox_discard": "Discard", "outbox_failed": "{count} changes could not be saved to the server.", "recent_searches": "Recent searches", + "remove_filter": "Remove filter {label}", "scope_all_folders": "All folders", "scope_this_folder": "This folder", "search_placeholder": "Search mail...", "showing_cached": "Showing cached mail", + "sort_toggle_hint": "Reverses the mail sort order", "swipe_move": "Move", "swipe_spam": "Spam", "total": "total", @@ -475,7 +490,18 @@ }, "offline": { "banner": "You are offline", - "queued_changes": "{count, plural, one {# change} other {# changes}} will sync when you reconnect" + "queued_changes": "{count, plural, one {# change} other {# changes}} will sync when you reconnect", + "sync": { + "cached_count": "{count, plural, one {# new message cached} other {# new messages cached}} • {size}", + "cancelled": "Sync cancelled", + "failed": "Offline sync failed", + "processed": "{completed}/{total} processed", + "ready": "Offline mail ready", + "syncing": "Syncing offline mail", + "title": "Offline sync", + "unable_to_download": "Unable to download", + "up_to_date": "Already up to date" + } }, "push_prompt": { "body": "Turn on background notifications so new messages reach you while the app is closed.", diff --git a/src/components/IdentitySheet.tsx b/src/components/IdentitySheet.tsx index 081e53de..65390aae 100644 --- a/src/components/IdentitySheet.tsx +++ b/src/components/IdentitySheet.tsx @@ -60,7 +60,10 @@ export function IdentitySheet({ {t('email_composer.from', 'From')} - + diff --git a/src/components/MoveSheet.tsx b/src/components/MoveSheet.tsx index cb9869e4..ac458760 100644 --- a/src/components/MoveSheet.tsx +++ b/src/components/MoveSheet.tsx @@ -92,7 +92,10 @@ export function MoveSheet({ {t('context_menu.move_to', 'Move to folder')} - + diff --git a/src/components/OfflineCacheBanner.tsx b/src/components/OfflineCacheBanner.tsx index c7c6d682..2aac398f 100644 --- a/src/components/OfflineCacheBanner.tsx +++ b/src/components/OfflineCacheBanner.tsx @@ -10,11 +10,13 @@ import { useUpdatesStore } from '../stores/updates-store'; import { formatBytes } from '../lib/offline-sync'; import { spacing, radius, typography, type ThemePalette } from '../theme/tokens'; import { useColors } from '../theme/colors'; +import { useLocaleStore } from '../stores/locale-store'; export function OfflineCacheBanner(): React.ReactElement | null { const c = useColors(); const styles = React.useMemo(() => makeStyles(c), [c]); const insets = useSafeAreaInsets(); + const t = useLocaleStore((s) => s.t); const sync = useOfflineCacheStore((s) => s.sync); const requestAbort = useOfflineCacheStore((s) => s.requestAbort); const resetSync = useOfflineCacheStore((s) => s.resetSync); @@ -32,9 +34,9 @@ export function OfflineCacheBanner(): React.ReactElement | null { // permanent fixture; the Settings screen still shows the cache stats. React.useEffect(() => { if (sync.phase === 'done' || sync.phase === 'cancelled') { - const t = setTimeout(() => resetSync(), 4000); - setHideTimer(t); - return () => clearTimeout(t); + const timer = setTimeout(() => resetSync(), 4000); + setHideTimer(timer); + return () => clearTimeout(timer); } if (hideTimer) clearTimeout(hideTimer); return undefined; @@ -48,30 +50,34 @@ export function OfflineCacheBanner(): React.ReactElement | null { ? Math.min(100, Math.round((sync.completed / sync.total) * 100)) : sync.phase === 'done' ? 100 : 0; - let title = 'Offline sync'; + let title = t('offline.sync.title', 'Offline sync'); let subtitle = ''; switch (sync.phase) { case 'scanning': - title = 'Syncing offline mail'; - subtitle = 'Scanning recent messages…'; + title = t('offline.sync.syncing', 'Syncing offline mail'); + subtitle = t('settings.offline.scanning', 'Scanning recent mail…'); break; case 'fetching': - title = 'Syncing offline mail'; + title = t('offline.sync.syncing', 'Syncing offline mail'); subtitle = `${sync.completed}/${sync.total} • ${formatBytes(sync.bytes)}`; break; case 'done': - title = 'Offline mail ready'; + title = t('offline.sync.ready', 'Offline mail ready'); subtitle = sync.fetched > 0 - ? `${sync.fetched} new message${sync.fetched === 1 ? '' : 's'} cached • ${formatBytes(sync.bytes)}` - : 'Already up to date'; + ? t( + 'offline.sync.cached_count', + '{count, plural, one {# new message cached} other {# new messages cached}} • {size}', + { count: sync.fetched, size: formatBytes(sync.bytes) }, + ) + : t('offline.sync.up_to_date', 'Already up to date'); break; case 'cancelled': - title = 'Sync cancelled'; - subtitle = `${sync.completed}/${sync.total} processed`; + title = t('offline.sync.cancelled', 'Sync cancelled'); + subtitle = t('offline.sync.processed', '{completed}/{total} processed', { completed: sync.completed, total: sync.total }); break; case 'error': - title = 'Offline sync failed'; - subtitle = sync.message ?? 'Unable to download'; + title = t('offline.sync.failed', 'Offline sync failed'); + subtitle = sync.message ?? t('offline.sync.unable_to_download', 'Unable to download'); break; default: return null; @@ -94,12 +100,18 @@ export function OfflineCacheBanner(): React.ReactElement | null { )} {showCancel && ( - - Cancel + + {t('common.cancel', 'Cancel')} )} {showDismiss && ( - + )} diff --git a/src/components/QrScanModal.tsx b/src/components/QrScanModal.tsx index dfcb1ecc..cc458cd7 100644 --- a/src/components/QrScanModal.tsx +++ b/src/components/QrScanModal.tsx @@ -76,7 +76,10 @@ export function QrScanModal({ visible, onClose, onScanned }: QrScanModalProps) { - + {t('login.mobile.qr_title', 'Sign-in code')} diff --git a/src/components/TagSheet.tsx b/src/components/TagSheet.tsx index 0a9734ed..19b94fbe 100644 --- a/src/components/TagSheet.tsx +++ b/src/components/TagSheet.tsx @@ -83,7 +83,10 @@ export function TagSheet({ visible, onClose, keywords, selectedEmails, onToggle {t('context_menu.items_selected', `${selectedEmails.length} emails selected`, { count: selectedEmails.length })} - + diff --git a/src/components/TemplateSheet.tsx b/src/components/TemplateSheet.tsx index 3737552b..4d0cb034 100644 --- a/src/components/TemplateSheet.tsx +++ b/src/components/TemplateSheet.tsx @@ -79,7 +79,10 @@ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps) {t('templates.picker_title', 'Choose a Template')} - + diff --git a/src/components/__tests__/screens-i18n.test.ts b/src/components/__tests__/screens-i18n.test.ts index aa81c6b4..0ad50169 100644 --- a/src/components/__tests__/screens-i18n.test.ts +++ b/src/components/__tests__/screens-i18n.test.ts @@ -12,6 +12,27 @@ const TRANSLATED_FILES: Record = { 'screens/FilesScreen.tsx': [], 'components/files/ShareSheet.tsx': [], 'components/files/FilePreviewModal.tsx': [], + // The empty date filter shows the input format. + 'screens/EmailListScreen.tsx': ['YYYY-MM-DD'], + 'screens/ScheduledScreen.tsx': [], + 'screens/EmailSourceScreen.tsx': [], + 'components/OfflineBanner.tsx': [], + 'components/OfflineCacheBanner.tsx': [], + 'components/UndoSnackbar.tsx': [], + 'components/UpdateBanner.tsx': [], + 'components/ToastHost.tsx': [], + 'components/PushOnboardingPrompt.tsx': [], + 'components/MoveSheet.tsx': [], + 'components/TagSheet.tsx': [], + 'components/TemplateSheet.tsx': [], + 'components/IdentitySheet.tsx': [], + 'components/QrScanModal.tsx': [], + 'components/email/ActionSheet.tsx': [], + 'components/email/AddressActionSheet.tsx': [], + 'components/email/CalendarInvitationBanner.tsx': [], + 'components/email/ListAttachmentChips.tsx': [], + 'components/email/ReadReceiptBanner.tsx': [], + 'components/email/UnsubscribeBanner.tsx': [], }; describe('translated screens', () => { diff --git a/src/components/email/ActionSheet.tsx b/src/components/email/ActionSheet.tsx index 0001ae2c..6c83cf95 100644 --- a/src/components/email/ActionSheet.tsx +++ b/src/components/email/ActionSheet.tsx @@ -5,6 +5,7 @@ import { X } from 'lucide-react-native'; import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens'; import { useColors } from '../../theme/colors'; import { useSheetDrag } from '../../lib/use-sheet-drag'; +import { useLocaleStore } from '../../stores/locale-store'; export interface ActionSheetItem { key: string; @@ -34,6 +35,7 @@ export function ActionSheet({ visible, title, subtitle, items, onClose, children const c = useColors(); const styles = React.useMemo(() => makeStyles(c), [c]); const insets = useSafeAreaInsets(); + const t = useLocaleStore((s) => s.t); const slideY = React.useRef(new Animated.Value(500)).current; const overlayOpacity = React.useRef(new Animated.Value(0)).current; const dragHandlers = useSheetDrag({ slideY, closedY: 500, onClose }); @@ -72,7 +74,10 @@ export function ActionSheet({ visible, title, subtitle, items, onClose, children {title} {subtitle ? {subtitle} : null} - + diff --git a/src/components/email/CalendarInvitationBanner.tsx b/src/components/email/CalendarInvitationBanner.tsx index e4fcaca9..e400d4e2 100644 --- a/src/components/email/CalendarInvitationBanner.tsx +++ b/src/components/email/CalendarInvitationBanner.tsx @@ -39,24 +39,25 @@ interface Props { jmapAccountId?: string; } -const TRUST_REASON_KEYS: Record, [string, string]> = { - sender_mismatch_unverified: [ - 'calendar.invitation.trust_sender_mismatch_unverified', - 'The sender does not match the organizer and the message is not authenticated.', - ], - authentication_failed: [ - 'calendar.invitation.trust_authentication_failed', - 'This message failed sender authentication (SPF/DKIM/DMARC).', - ], - sender_mismatch: [ - 'calendar.invitation.trust_sender_mismatch', - 'The sender differs from the event organizer.', - ], - authentication_missing: [ - 'calendar.invitation.trust_authentication_missing', - 'The sender could not be verified.', - ], -}; +// Literal t() calls so the keys are harvested into the catalog. +function trustReasonText( + reason: NonNullable, + t: (key: string, fallback?: string) => string, +): string { + switch (reason) { + case 'sender_mismatch_unverified': + return t( + 'calendar.invitation.trust_sender_mismatch_unverified', + 'The sender does not match the organizer and the message is not authenticated.', + ); + case 'authentication_failed': + return t('calendar.invitation.trust_authentication_failed', 'This message failed sender authentication (SPF/DKIM/DMARC).'); + case 'sender_mismatch': + return t('calendar.invitation.trust_sender_mismatch', 'The sender differs from the event organizer.'); + case 'authentication_missing': + return t('calendar.invitation.trust_authentication_missing', 'The sender could not be verified.'); + } +} export function CalendarInvitationBanner({ email, jmapAccountId }: Props) { const c = useColors(); @@ -264,7 +265,7 @@ export function CalendarInvitationBanner({ email, jmapAccountId }: Props) { )} {trust.reason - ? t(TRUST_REASON_KEYS[trust.reason][0], TRUST_REASON_KEYS[trust.reason][1]) + ? trustReasonText(trust.reason, t) : t('calendar.invitation.trust_verified', 'Sender verified')} @@ -287,7 +288,7 @@ export function CalendarInvitationBanner({ email, jmapAccountId }: Props) { {organizer ? ( } - text={`${t('calendar.invitation.organizer', 'Organizer')}: ${organizer}`} + text={t('email_viewer.calendar_invitation.organizer', 'Organized by {name}', { name: organizer })} styles={styles} /> ) : null} diff --git a/src/screens/EmailListScreen.tsx b/src/screens/EmailListScreen.tsx index 377c57b0..7d71bef9 100644 --- a/src/screens/EmailListScreen.tsx +++ b/src/screens/EmailListScreen.tsx @@ -52,8 +52,8 @@ import { getFullEmail, emptyMailbox as apiEmptyMailbox } from '../api/email'; import type { RootStackParamList } from '../navigation/types'; import type { Attachment, Email } from '../api/types'; -function getSenderName(email: Email): string { - return email.from?.[0]?.name || email.from?.[0]?.email || 'Unknown'; +function getSenderName(email: Email, unknownLabel: string): string { + return email.from?.[0]?.name || email.from?.[0]?.email || unknownLabel; } function getSenderEmail(email: Email): string | undefined { @@ -61,12 +61,12 @@ function getSenderEmail(email: Email): string | undefined { } // Sent/Drafts rows name the recipient, not "me" (webmail 1.4.12). -function getCounterpart(email: Email, showRecipient: boolean): { name: string; email?: string } { +function getCounterpart(email: Email, showRecipient: boolean, unknownLabel: string): { name: string; email?: string } { if (showRecipient) { const to = email.to?.[0] ?? email.cc?.[0] ?? email.bcc?.[0]; if (to) return { name: to.name || to.email, email: to.email }; } - return { name: getSenderName(email), email: getSenderEmail(email) }; + return { name: getSenderName(email, unknownLabel), email: getSenderEmail(email) }; } function isUnread(email: Email): boolean { @@ -128,7 +128,11 @@ const EmailRow = React.memo(function EmailRow({ const timeFormat = useSettingsStore((s) => s.timeFormat); const locale = useLocaleStore((s) => s.locale); const tr = useLocaleStore((s) => s.t); - const { name: senderName, email: senderEmail } = getCounterpart(item, showRecipient); + const { name: senderName, email: senderEmail } = getCounterpart( + item, + showRecipient, + tr('email_viewer.unknown_sender', 'Unknown'), + ); const unread = isUnread(item); const starred = isStarred(item); const pinned = isPinned(item); @@ -493,12 +497,12 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL })); const { imported, failed } = await importEmails(files, targetMailboxId); Alert.alert( - 'Import', + t('email_list.import.title', 'Import'), failed > 0 - ? `${imported} imported, ${failed} failed.` + ? t('email_list.import.partial', '{imported} imported, {failed} failed.', { imported, failed }) : imported === 0 - ? 'No messages were imported.' - : `${imported} message${imported === 1 ? '' : 's'} imported.`, + ? t('email_list.import.none', 'No messages were imported.') + : t('email_list.import.success', '{count, plural, one {# message imported.} other {# messages imported.}}', { count: imported }), ); } finally { setImporting(false); @@ -965,14 +969,23 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL {/* Header */} {selectionMode ? ( - + - {selectedIds.size} selected + + {t('email_list.batch_actions.selected_messages', '{count, plural, one {1 email} other {# emails}} selected', { count: selectedIds.size })} + { void handleBulkStar(); }} style={styles.headerButton} hitSlop={6} + accessibilityRole="button" + accessibilityLabel={allSelectedAreStarred ? t('context_menu.unstar', 'Unstar') : t('context_menu.star', 'Star')} > { void handleBulkMarkReadToggle(); }} style={styles.headerButton} hitSlop={6} + accessibilityRole="button" + accessibilityLabel={allSelectedAreRead + ? t('email_list.batch_actions.mark_unread', 'Mark as unread') + : t('email_list.batch_actions.mark_read', 'Mark as read')} > {allSelectedAreRead ? ( @@ -995,6 +1012,8 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL onPress={() => setTagSheetOpen(true)} style={styles.headerButton} hitSlop={6} + accessibilityRole="button" + accessibilityLabel={t('context_menu.tag', 'Tag')} > @@ -1002,6 +1021,8 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL onPress={() => setBatchMoveOpen(true)} style={styles.headerButton} hitSlop={6} + accessibilityRole="button" + accessibilityLabel={t('email_viewer.move', 'Move')} > @@ -1010,6 +1031,7 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL onPress={() => { void handleBulkSpam(); }} style={styles.headerButton} hitSlop={6} + accessibilityRole="button" accessibilityLabel={inJunk ? t('context_menu.not_spam', 'Not spam') : t('context_menu.mark_as_spam', 'Report spam')} > {inJunk ? ( @@ -1024,6 +1046,8 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL onPress={() => { void handleBulkArchive(); }} style={styles.headerButton} hitSlop={6} + accessibilityRole="button" + accessibilityLabel={t('context_menu.archive', 'Archive')} > @@ -1032,13 +1056,20 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL onPress={() => { void handleBulkDelete(); }} style={styles.headerButton} hitSlop={6} + accessibilityRole="button" + accessibilityLabel={t('email_list.batch_actions.delete', 'Delete')} > ) : ( - setDrawerOpen(true)} style={styles.headerButton}> + setDrawerOpen(true)} + style={styles.headerButton} + accessibilityRole="button" + accessibilityLabel={t('sidebar.mobile.toggle_menu', 'Toggle menu')} + > @@ -1050,6 +1081,8 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL style={styles.headerButton} disabled={importing} hitSlop={6} + accessibilityRole="button" + accessibilityLabel={t('email_viewer.import_email', 'Import .eml or .zip')} > {importing ? ( @@ -1067,7 +1100,14 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL {/* Search bar (always visible) */} - + {allSelected ? ( ) : selectionMode ? ( @@ -1099,6 +1139,8 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL onPress={() => { setSearchInput(''); setSearchQuery(''); }} hitSlop={8} style={styles.searchClearButton} + accessibilityRole="button" + accessibilityLabel={t('contacts.clear_search', 'Clear search')} > @@ -1108,8 +1150,10 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL style={styles.filterButton} onPress={() => setSortAscending(!sortAscending)} accessibilityRole="button" - accessibilityLabel={sortAscending ? 'Sorted oldest first' : 'Sorted newest first'} - accessibilityHint="Reverses the mail sort order" + accessibilityLabel={sortAscending + ? t('settings.appearance.message_list_order.direction.oldest_first', 'Oldest first') + : t('settings.appearance.message_list_order.direction.newest_first', 'Newest first')} + accessibilityHint={t('email_list.sort_toggle_hint', 'Reverses the mail sort order')} > {sortAscending ? ( @@ -1120,6 +1164,9 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL 0 && styles.filterButtonActive]} onPress={() => setFilterMenuOpen(true)} + accessibilityRole="button" + accessibilityLabel={t('advanced_search.advanced_filters_tooltip', 'Advanced search filters')} + accessibilityValue={activeFilterCount > 0 ? { text: String(activeFilterCount) } : undefined} > 0 ? c.primary : c.textMuted} /> {activeFilterCount > 0 && ( @@ -1141,7 +1188,12 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL {q} - removeRecentSearch(q)} hitSlop={8}> + removeRecentSearch(q)} + hitSlop={8} + accessibilityRole="button" + accessibilityLabel={t('advanced_search.suggestions_remove_recent', 'Remove from recent searches')} + > @@ -1202,28 +1254,40 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL /> ) : null} {filters.from ? ( - setFilterField('from', undefined)} /> + setFilterField('from', undefined)} + /> ) : null} {filters.to ? ( - setFilterField('to', undefined)} /> + setFilterField('to', undefined)} + /> ) : null} {filters.subject ? ( - setFilterField('subject', undefined)} /> + setFilterField('subject', undefined)} + /> ) : null} {filters.body ? ( - setFilterField('body', undefined)} /> + setFilterField('body', undefined)} + /> ) : null} {filters.dateAfter ? ( } - label={`${t('advanced_search.after', 'After')} ${filters.dateAfter}`} + label={t('email_list.filter_chip', '{field}: {value}', { field: t('advanced_search.date_after', 'After'), value: filters.dateAfter })} onRemove={() => setFilterField('dateAfter', undefined)} /> ) : null} {filters.dateBefore ? ( } - label={`${t('advanced_search.before', 'Before')} ${filters.dateBefore}`} + label={t('email_list.filter_chip', '{field}: {value}', { field: t('advanced_search.date_before', 'Before'), value: filters.dateBefore })} onRemove={() => setFilterField('dateBefore', undefined)} /> ) : null} @@ -1232,7 +1296,7 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL icon={filters.isUnread ? : } - label={filters.isUnread ? t('email_list.unread', 'Unread') : t('advanced_search.read', 'Read')} + label={filters.isUnread ? t('advanced_search.unread', 'Unread') : t('advanced_search.read', 'Read')} onRemove={() => cycleTriStateTo('isUnread', undefined)} /> )} @@ -1259,6 +1323,7 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL }} style={styles.clearAllButton} hitSlop={6} + accessibilityRole="button" > {t('advanced_search.clear', 'Clear')} @@ -1352,7 +1417,10 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL {currentMailbox && !hasActiveSearchOrFilter ? ( - {currentMailbox.totalEmails} {t('email_list.total', 'total')} · {currentMailbox.unreadEmails} {t('email_list.unread', 'unread')} + {t('email_list.folder_counts', '{total} total · {unread} unread', { + total: currentMailbox.totalEmails, + unread: currentMailbox.unreadEmails, + })} ) : null} @@ -1377,6 +1445,8 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL [styles.fab, pressed && styles.fabPressed]} + accessibilityRole="button" + accessibilityLabel={t('sidebar.mobile.compose', 'Compose')} > @@ -1395,20 +1465,23 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL - Filter emails + {t('email_list.filter_title', 'Filter emails')} setFilters({})} style={styles.filterMenuHeaderBtn} hitSlop={6} + accessibilityRole="button" > - Clear + {t('advanced_search.clear', 'Clear')} setFilterMenuOpen(false)} style={styles.filterMenuClose} hitSlop={6} + accessibilityRole="button" + accessibilityLabel={t('common.close', 'Close')} > @@ -1418,11 +1491,11 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL - From + {t('advanced_search.from', 'From')} setFilterField('from', v)} - placeholder="sender@example.com" + placeholder={t('advanced_search.from_placeholder', 'Sender email or name')} placeholderTextColor={c.textMuted} autoCapitalize="none" autoCorrect={false} @@ -1430,11 +1503,11 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL /> - To + {t('advanced_search.to', 'To')} setFilterField('to', v)} - placeholder="recipient@example.com" + placeholder={t('advanced_search.to_placeholder', 'Recipient email or name')} placeholderTextColor={c.textMuted} autoCapitalize="none" autoCorrect={false} @@ -1496,7 +1569,7 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL - After + {t('advanced_search.date_after', 'After')} setDatePickerField('dateAfter')} @@ -1509,6 +1582,8 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL setFilterField('dateAfter', undefined)} hitSlop={6} + accessibilityRole="button" + accessibilityLabel={t('advanced_search.clear', 'Clear')} > @@ -1516,7 +1591,7 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL - Before + {t('advanced_search.date_before', 'Before')} setDatePickerField('dateBefore')} @@ -1529,6 +1604,8 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL setFilterField('dateBefore', undefined)} hitSlop={6} + accessibilityRole="button" + accessibilityLabel={t('advanced_search.clear', 'Clear')} > @@ -1540,13 +1617,13 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL } - label="Has attachment" + label={t('email_list.has_attachment', 'Has attachment')} value={filters.hasAttachment} onPress={() => cycleTriState('hasAttachment')} /> } - label="Starred" + label={t('advanced_search.starred', 'Starred')} value={filters.isStarred} onPress={() => cycleTriState('isStarred')} /> @@ -1558,7 +1635,7 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL ) } - label={filters.isUnread === false ? 'Read' : 'Unread'} + label={filters.isUnread === false ? t('advanced_search.read', 'Read') : t('advanced_search.unread', 'Unread')} value={filters.isUnread} onPress={() => cycleTriState('isUnread')} /> @@ -1588,7 +1665,7 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL setDatePickerField(null)} hitSlop={8}> - Done + {t('common.done', 'Done')} makeStyles(c), [c]); + const t = useLocaleStore((s) => s.t); return ( - + {icon} {label} {onRemove ? ( - + ) : null} @@ -1681,7 +1770,12 @@ function ScopeChip({ label, active, onPress }: { label: string; active: boolean; const c = useColors(); const styles = React.useMemo(() => makeStyles(c), [c]); return ( - + {label} ); @@ -1710,6 +1804,8 @@ function TriToggle({ state === 'on' && styles.triToggleOn, state === 'off' && styles.triToggleOff, ]} + accessibilityRole="button" + accessibilityState={{ selected: state !== 'unset' }} > {icon} - navigation.goBack()} style={styles.headerBtn} hitSlop={8}> + navigation.goBack()} + style={styles.headerBtn} + hitSlop={8} + accessibilityRole="button" + accessibilityLabel={t('common.back', 'Back')} + > @@ -82,7 +88,14 @@ export default function EmailSourceScreen({ route, navigation }: Props) { > {copied ? : } - + diff --git a/src/screens/ScheduledScreen.tsx b/src/screens/ScheduledScreen.tsx index 6146b12d..c09b2169 100644 --- a/src/screens/ScheduledScreen.tsx +++ b/src/screens/ScheduledScreen.tsx @@ -277,7 +277,13 @@ export default function ScheduledScreen({ navigation }: Props) { return ( - navigation.goBack()} style={styles.headerBtn} hitSlop={8}> + navigation.goBack()} + style={styles.headerBtn} + hitSlop={8} + accessibilityRole="button" + accessibilityLabel={t('common.back', 'Back')} + > {t('sidebar.scheduled', 'Scheduled')}