mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-04 08:07:29 +00:00
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:
@@ -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",
|
||||||
|
|||||||
@@ -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.
|
||||||
'-- Jane Doe Bulwark Mail',
|
'-- Jane Doe 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([]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user