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": {
"title": "Experimental Feature"
},
"filters": {
"remove_action": "Remove action",
"remove_condition": "Remove condition",
"sieve_editor": {
"line_count": "{count, plural, one {# line} other {# lines}}"
}
},
"folders": {
"account": "Account",
"cannot_delete": "Cannot delete",
+34 -12
View File
@@ -5,27 +5,49 @@ import { describe, it, expect } from 'vitest';
import { join } from 'node:path';
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[]> = {
'ReadingSettings.tsx': [],
'ContactsSettings.tsx': [],
'ContentSendersSettings.tsx': [],
'LayoutSettings.tsx': [],
'CalendarSettings.tsx': [],
'IdentitySettings.tsx': [
// Example signatures in the placeholders: sample data, not UI text.
'settings/AboutDataSettings.tsx': ['MB', 'GitHub'],
'settings/AccountSecuritySettings.tsx': ['AES-128', 'AES-256'],
'settings/AccountSettings.tsx': [],
'settings/AppearanceSettings.tsx': [],
'settings/CalendarSettings.tsx': [],
// Sub-address delimiter samples ("user+tag").
'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',
'<p><b>Jane Doe</b><br>Bulwark Mail</p>',
],
'FilterSettings.tsx': [],
'SidebarAppsSettings.tsx': ['https://example.com'],
'TemplateSettings.tsx': ['JSON', '{"templates": [...]}'],
// The raw keyword prefix shown next to a tag id.
'settings/KeywordSettings.tsx': ['$label:'],
'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', () => {
for (const [file, allow] of Object.entries(TRANSLATED_PANES)) {
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([]);
});
}
+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>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<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} />
</Pressable>
<Text style={styles.headerTitle}>
@@ -295,6 +301,8 @@ export function FilterRuleModal({ visible, rule, mailboxes, onSave, onClose }: F
disabled={conditions.length <= 1}
hitSlop={8}
style={[styles.removeBtn, conditions.length <= 1 && styles.removeBtnDisabled]}
accessibilityRole="button"
accessibilityLabel={t('settings.filters.remove_condition', 'Remove condition')}
>
<Trash2 size={16} color={c.mutedForeground} />
</Pressable>
@@ -361,6 +369,8 @@ export function FilterRuleModal({ visible, rule, mailboxes, onSave, onClose }: F
disabled={actions.length <= 1}
hitSlop={8}
style={[styles.removeBtn, actions.length <= 1 && styles.removeBtnDisabled]}
accessibilityRole="button"
accessibilityLabel={t('settings.filters.remove_action', 'Remove action')}
>
<Trash2 size={16} color={c.mutedForeground} />
</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>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<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} />
</Pressable>
<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>
<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 && (
<View style={[styles.resultBanner, validationResult.isValid ? styles.resultOk : styles.resultErr]}>
@@ -268,7 +268,7 @@ export function AboutDataSettings() {
: sync.phase === 'scanning'
? t('settings.offline.scanning', 'Scanning recent mail…')
: 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) })}
</Text>
{queuedChanges > 0 && (
@@ -205,7 +205,12 @@ export function ComposingSettings() {
{attachmentReminderKeywords.map((kw) => (
<View key={kw} style={styles.chip}>
<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} />
</Pressable>
</View>
+11 -1
View File
@@ -408,6 +408,8 @@ export function FilterSettings({ onOpenVacation }: FilterSettingsProps = {}) {
disabled={editIdx <= 0}
hitSlop={6}
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} />
</Pressable>
@@ -416,12 +418,20 @@ export function FilterSettings({ onOpenVacation }: FilterSettingsProps = {}) {
disabled={editIdx === editableIds.length - 1}
hitSlop={6}
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} />
</Pressable>
</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} />
</Pressable>
</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.new_folder', 'New folder')}
</Text>
<Pressable onPress={closeEditor} hitSlop={8}>
<Pressable
onPress={closeEditor}
hitSlop={8}
accessibilityRole="button"
accessibilityLabel={t('common.close', 'Close')}
>
<X size={20} color={c.text} />
</Pressable>
</View>
@@ -232,6 +232,8 @@ export function ReadingSettings() {
)
}
hitSlop={6}
accessibilityRole="button"
accessibilityLabel={t('settings.email_behavior.attachment_reminder.remove', 'Remove')}
>
<X size={12} color={c.mutedForeground} />
</Pressable>