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')}