fix: label the remaining settings icon buttons for screen readers

The filter rule reorder, delete and remove-condition/action buttons,
the filter, Sieve and folder editor close buttons and the attachment
keyword remove buttons had no label. The Sieve editor's line count now
uses an ICU plural, and the settings i18n test covers every pane.
This commit is contained in:
Linus Rath
2026-09-24 20:49:56 +02:00
parent 455608f9f0
commit c1e5cd6c8b
9 changed files with 88 additions and 19 deletions
+7
View File
@@ -640,6 +640,13 @@
"experimental": { "experimental": {
"title": "Experimental Feature" "title": "Experimental Feature"
}, },
"filters": {
"remove_action": "Remove action",
"remove_condition": "Remove condition",
"sieve_editor": {
"line_count": "{count, plural, one {# line} other {# lines}}"
}
},
"folders": { "folders": {
"account": "Account", "account": "Account",
"cannot_delete": "Cannot delete", "cannot_delete": "Cannot delete",
+34 -12
View File
@@ -5,27 +5,49 @@ import { describe, it, expect } from 'vitest';
import { join } from 'node:path'; import { join } from 'node:path';
import { findUntranslatedLiterals } from './untranslated-literals'; import { findUntranslatedLiterals } from './untranslated-literals';
// Pane file → texts that stay as they are on purpose. // Pane file (relative to src/components/) → texts that stay as they are on
// purpose: product names, units, format patterns and sample data.
const TRANSLATED_PANES: Record<string, string[]> = { const TRANSLATED_PANES: Record<string, string[]> = {
'ReadingSettings.tsx': [], 'settings/AboutDataSettings.tsx': ['MB', 'GitHub'],
'ContactsSettings.tsx': [], 'settings/AccountSecuritySettings.tsx': ['AES-128', 'AES-256'],
'ContentSendersSettings.tsx': [], 'settings/AccountSettings.tsx': [],
'LayoutSettings.tsx': [], 'settings/AppearanceSettings.tsx': [],
'CalendarSettings.tsx': [], 'settings/CalendarSettings.tsx': [],
'IdentitySettings.tsx': [ // Sub-address delimiter samples ("user+tag").
// Example signatures in the placeholders: sample data, not UI text. 'settings/ComposingSettings.tsx': ['user tag'],
'settings/ContactsSettings.tsx': [],
'settings/ContentSendersSettings.tsx': [],
'settings/DownloadsSettings.tsx': [],
'settings/FilesSettings.tsx': [],
'settings/FilterSettings.tsx': [],
'settings/FolderSettings.tsx': [],
'settings/IdentitySettings.tsx': [
// Example signatures in the placeholders.
'--&#10;Jane Doe&#10;Bulwark Mail', '--&#10;Jane Doe&#10;Bulwark Mail',
'<p><b>Jane Doe</b><br>Bulwark Mail</p>', '<p><b>Jane Doe</b><br>Bulwark Mail</p>',
], ],
'FilterSettings.tsx': [], // The raw keyword prefix shown next to a tag id.
'SidebarAppsSettings.tsx': ['https://example.com'], 'settings/KeywordSettings.tsx': ['$label:'],
'TemplateSettings.tsx': ['JSON', '{"templates": [...]}'], 'settings/LanguageSettings.tsx': [],
'settings/LayoutSettings.tsx': [],
'settings/NotificationSettings.tsx': [],
'settings/PluginsSettings.tsx': [],
'settings/ReadingSettings.tsx': [],
'settings/SidebarAppsSettings.tsx': ['https://example.com'],
'settings/SmimeSettings.tsx': [],
'settings/TemplateSettings.tsx': ['JSON', '{"templates": [...]}'],
'settings/ThemesSettings.tsx': [],
'settings/UpdatesSettings.tsx': [],
'settings/VacationSettings.tsx': ['YYYY-MM-DD HH:MM'],
'settings/settings-section.tsx': [],
'filters/FilterRuleModal.tsx': [],
'filters/SieveEditorSheet.tsx': [],
}; };
describe('translated settings panes', () => { describe('translated settings panes', () => {
for (const [file, allow] of Object.entries(TRANSLATED_PANES)) { for (const [file, allow] of Object.entries(TRANSLATED_PANES)) {
it(`${file} has no hard-coded English text`, () => { it(`${file} has no hard-coded English text`, () => {
const literals = findUntranslatedLiterals(join(__dirname, '..', 'settings', file), allow); const literals = findUntranslatedLiterals(join(__dirname, '..', file), allow);
expect(literals).toEqual([]); expect(literals).toEqual([]);
}); });
} }
+11 -1
View File
@@ -229,7 +229,13 @@ export function FilterRuleModal({ visible, rule, mailboxes, onSave, onClose }: F
<Modal visible={visible} animationType="slide" onRequestClose={onClose} statusBarTranslucent> <Modal visible={visible} animationType="slide" onRequestClose={onClose} statusBarTranslucent>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.header}> <View style={styles.header}>
<Pressable onPress={onClose} hitSlop={8} style={styles.headerClose}> <Pressable
onPress={onClose}
hitSlop={8}
style={styles.headerClose}
accessibilityRole="button"
accessibilityLabel={t('common.close', 'Close')}
>
<X size={20} color={c.text} /> <X size={20} color={c.text} />
</Pressable> </Pressable>
<Text style={styles.headerTitle}> <Text style={styles.headerTitle}>
@@ -295,6 +301,8 @@ export function FilterRuleModal({ visible, rule, mailboxes, onSave, onClose }: F
disabled={conditions.length <= 1} disabled={conditions.length <= 1}
hitSlop={8} hitSlop={8}
style={[styles.removeBtn, conditions.length <= 1 && styles.removeBtnDisabled]} style={[styles.removeBtn, conditions.length <= 1 && styles.removeBtnDisabled]}
accessibilityRole="button"
accessibilityLabel={t('settings.filters.remove_condition', 'Remove condition')}
> >
<Trash2 size={16} color={c.mutedForeground} /> <Trash2 size={16} color={c.mutedForeground} />
</Pressable> </Pressable>
@@ -361,6 +369,8 @@ export function FilterRuleModal({ visible, rule, mailboxes, onSave, onClose }: F
disabled={actions.length <= 1} disabled={actions.length <= 1}
hitSlop={8} hitSlop={8}
style={[styles.removeBtn, actions.length <= 1 && styles.removeBtnDisabled]} style={[styles.removeBtn, actions.length <= 1 && styles.removeBtnDisabled]}
accessibilityRole="button"
accessibilityLabel={t('settings.filters.remove_action', 'Remove action')}
> >
<Trash2 size={16} color={c.mutedForeground} /> <Trash2 size={16} color={c.mutedForeground} />
</Pressable> </Pressable>
+10 -2
View File
@@ -65,7 +65,13 @@ export function SieveEditorSheet({ visible, content, onSave, onClose, onValidate
<Modal visible={visible} animationType="slide" onRequestClose={onClose} statusBarTranslucent> <Modal visible={visible} animationType="slide" onRequestClose={onClose} statusBarTranslucent>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.header}> <View style={styles.header}>
<Pressable onPress={onClose} hitSlop={8} style={styles.headerClose}> <Pressable
onPress={onClose}
hitSlop={8}
style={styles.headerClose}
accessibilityRole="button"
accessibilityLabel={t('common.close', 'Close')}
>
<X size={20} color={c.text} /> <X size={20} color={c.text} />
</Pressable> </Pressable>
<Text style={styles.headerTitle}>{t('settings.filters.sieve_editor.title', 'Sieve Script Editor')}</Text> <Text style={styles.headerTitle}>{t('settings.filters.sieve_editor.title', 'Sieve Script Editor')}</Text>
@@ -104,7 +110,9 @@ export function SieveEditorSheet({ visible, content, onSave, onClose, onValidate
/> />
</ScrollView> </ScrollView>
<Text style={styles.lineCount}>{lineCount} {lineCount === 1 ? 'line' : 'lines'}</Text> <Text style={styles.lineCount}>
{t('settings.filters.sieve_editor.line_count', '{count, plural, one {# line} other {# lines}}', { count: lineCount })}
</Text>
{validationResult && ( {validationResult && (
<View style={[styles.resultBanner, validationResult.isValid ? styles.resultOk : styles.resultErr]}> <View style={[styles.resultBanner, validationResult.isValid ? styles.resultOk : styles.resultErr]}>
@@ -268,7 +268,7 @@ export function AboutDataSettings() {
: sync.phase === 'scanning' : sync.phase === 'scanning'
? t('settings.offline.scanning', 'Scanning recent mail…') ? t('settings.offline.scanning', 'Scanning recent mail…')
: sync.phase === 'error' : sync.phase === 'error'
? t('settings.offline.last_sync_failed', 'Last sync failed: {message}', { message: sync.message ?? 'unknown error' }) ? t('settings.offline.last_sync_failed', 'Last sync failed: {message}', { message: sync.message ?? t('identities.validation_errors.unknown_error', 'Unknown error') })
: t('settings.offline.last_sync', 'Last sync {when}', { when: formatRelativeTime(sync.finishedAt) })} : t('settings.offline.last_sync', 'Last sync {when}', { when: formatRelativeTime(sync.finishedAt) })}
</Text> </Text>
{queuedChanges > 0 && ( {queuedChanges > 0 && (
@@ -205,7 +205,12 @@ export function ComposingSettings() {
{attachmentReminderKeywords.map((kw) => ( {attachmentReminderKeywords.map((kw) => (
<View key={kw} style={styles.chip}> <View key={kw} style={styles.chip}>
<Text style={styles.chipText}>{kw}</Text> <Text style={styles.chipText}>{kw}</Text>
<Pressable onPress={() => removeKeyword(kw)} hitSlop={6}> <Pressable
onPress={() => removeKeyword(kw)}
hitSlop={6}
accessibilityRole="button"
accessibilityLabel={t('settings.email_behavior.attachment_reminder.remove', 'Remove')}
>
<X size={12} color={c.textSecondary} /> <X size={12} color={c.textSecondary} />
</Pressable> </Pressable>
</View> </View>
+11 -1
View File
@@ -408,6 +408,8 @@ export function FilterSettings({ onOpenVacation }: FilterSettingsProps = {}) {
disabled={editIdx <= 0} disabled={editIdx <= 0}
hitSlop={6} hitSlop={6}
style={[styles.reorderBtn, editIdx <= 0 && styles.reorderBtnDisabled]} style={[styles.reorderBtn, editIdx <= 0 && styles.reorderBtnDisabled]}
accessibilityRole="button"
accessibilityLabel={t('settings.appearance.message_list_order.move_up', 'Move up')}
> >
<ChevronUp size={16} color={c.mutedForeground} /> <ChevronUp size={16} color={c.mutedForeground} />
</Pressable> </Pressable>
@@ -416,12 +418,20 @@ export function FilterSettings({ onOpenVacation }: FilterSettingsProps = {}) {
disabled={editIdx === editableIds.length - 1} disabled={editIdx === editableIds.length - 1}
hitSlop={6} hitSlop={6}
style={[styles.reorderBtn, editIdx === editableIds.length - 1 && styles.reorderBtnDisabled]} style={[styles.reorderBtn, editIdx === editableIds.length - 1 && styles.reorderBtnDisabled]}
accessibilityRole="button"
accessibilityLabel={t('settings.appearance.message_list_order.move_down', 'Move down')}
> >
<ChevronDown size={16} color={c.mutedForeground} /> <ChevronDown size={16} color={c.mutedForeground} />
</Pressable> </Pressable>
</View> </View>
<Pressable onPress={() => handleDelete(rule)} hitSlop={6} style={styles.deleteBtn}> <Pressable
onPress={() => handleDelete(rule)}
hitSlop={6}
style={styles.deleteBtn}
accessibilityRole="button"
accessibilityLabel={t('settings.filters.delete_rule', 'Delete Rule')}
>
<X size={16} color={c.mutedForeground} /> <X size={16} color={c.mutedForeground} />
</Pressable> </Pressable>
</View> </View>
+6 -1
View File
@@ -306,7 +306,12 @@ export function FolderSettings() {
? t('settings.folders.edit_folder', `Edit "${editor.mailbox.name}"`, { name: editor.mailbox.name }) ? t('settings.folders.edit_folder', `Edit "${editor.mailbox.name}"`, { name: editor.mailbox.name })
: t('settings.folders.new_folder', 'New folder')} : t('settings.folders.new_folder', 'New folder')}
</Text> </Text>
<Pressable onPress={closeEditor} hitSlop={8}> <Pressable
onPress={closeEditor}
hitSlop={8}
accessibilityRole="button"
accessibilityLabel={t('common.close', 'Close')}
>
<X size={20} color={c.text} /> <X size={20} color={c.text} />
</Pressable> </Pressable>
</View> </View>
@@ -232,6 +232,8 @@ export function ReadingSettings() {
) )
} }
hitSlop={6} hitSlop={6}
accessibilityRole="button"
accessibilityLabel={t('settings.email_behavior.attachment_reminder.remove', 'Remove')}
> >
<X size={12} color={c.mutedForeground} /> <X size={12} color={c.mutedForeground} />
</Pressable> </Pressable>