fix: translate the composer's leftovers and label its header buttons

Group recipient chips, the group size in suggestions, the upload
progress and the upload/send failure fallbacks were English or glued
together from a translated word and a number; they now use ICU
messages. The close, attach, schedule and sender-options buttons, the
recipient and attachment remove buttons and the From override switch
get screen reader labels (audit U8).
This commit is contained in:
Linus Rath
2026-09-24 20:49:55 +02:00
parent 9c0c35c7ba
commit 771558d8c7
3 changed files with 58 additions and 16 deletions
+5
View File
@@ -226,6 +226,8 @@
"editor_unavailable": "Could not read the message content. Copy your text, then close and reopen the composer.",
"format_plain_text": "Switch to plain text",
"format_rich_text": "Switch to rich text (HTML)",
"from_options": "Sender options",
"group_chip": "{name} ({count})",
"identity_unavailable": "Identity unavailable",
"image_load_failed": "Could not load image",
"no_recipient": "(no recipient)",
@@ -236,6 +238,7 @@
"recipient_copy": "Copy address",
"recipient_move_to": "Move to {field}",
"recipient_remove": "Remove",
"remove_attachment": "Remove attachment",
"save_draft": "Save Draft",
"save_failed_no_drafts": "There is no Drafts folder to save this draft to.",
"save_failed_no_identity": "Your sender identities could not be loaded, so this draft cannot be saved. Check your connection, or copy your text before closing.",
@@ -260,7 +263,9 @@
"remove_background_color": "Remove background color"
},
"undo_send_label": "Sending in {seconds}s…",
"upload_failed_short": "Upload failed",
"uploading": "Uploading...",
"uploading_pct": "Uploading {pct}%",
"validation": {
"subject_required": "Add a subject"
}
@@ -14,6 +14,9 @@ const TRANSLATED_FILES: Record<string, string[]> = {
'components/files/FilePreviewModal.tsx': [],
// The empty date filter shows the input format.
'screens/EmailListScreen.tsx': ['YYYY-MM-DD'],
// The link prompt shows an example URL.
'screens/ComposeScreen.tsx': ['https://example.com'],
'components/RichTextEditor.tsx': [],
'screens/ScheduledScreen.tsx': [],
'screens/EmailSourceScreen.tsx': [],
'components/OfflineBanner.tsx': [],
+50 -16
View File
@@ -192,8 +192,12 @@ function RecipientChip({
}) {
const c = useColors();
const styles = React.useMemo(() => makeStyles(c), [c]);
const t = useLocaleStore((s) => s.t);
const label = recipient.group
? `${recipient.name || 'Group'} (${recipient.group.members.length})`
? t('email_composer.group_chip', '{name} ({count})', {
name: recipient.name || t('contacts.group', 'Group'),
count: recipient.group.members.length,
})
: recipient.name || recipient.email;
return (
<Pressable onLongPress={onLongPress} delayLongPress={350} style={[styles.chip, invalid && styles.chipInvalid]}>
@@ -201,7 +205,12 @@ function RecipientChip({
<Text style={[styles.chipText, invalid && styles.chipTextInvalid]} numberOfLines={1}>
{label}
</Text>
<Pressable onPress={onRemove} hitSlop={8}>
<Pressable
onPress={onRemove}
hitSlop={8}
accessibilityRole="button"
accessibilityLabel={t('email_composer.recipient_remove', 'Remove')}
>
<X size={12} color={invalid ? c.error : c.textMuted} />
</Pressable>
</Pressable>
@@ -225,6 +234,7 @@ function SuggestionList({
}) {
const c = useColors();
const styles = React.useMemo(() => makeStyles(c), [c]);
const t = useLocaleStore((s) => s.t);
return (
<View style={styles.suggestionBox}>
{suggestions.map((s, i) => (
@@ -244,7 +254,9 @@ function SuggestionList({
{s.name || s.email}
</Text>
{s.group ? (
<Text style={styles.suggestionEmail} numberOfLines={1}>{`${s.group.memberCount} ✉`}</Text>
<Text style={styles.suggestionEmail} numberOfLines={1}>
{t('contacts.groups.member_count', '{count, plural, =0 {No members} one {1 member} other {# members}}', { count: s.group.memberCount })}
</Text>
) : !!s.name && (
<Text style={styles.suggestionEmail} numberOfLines={1}>{s.email}</Text>
)}
@@ -256,16 +268,15 @@ function SuggestionList({
}
function AttachmentChip({
attachment, onRemove, onPress, uploadingLabel, cancelLabel,
attachment, onRemove, onPress,
}: {
attachment: AttachmentEntry;
onRemove: () => void;
onPress: () => void;
uploadingLabel: string;
cancelLabel: string;
}) {
const c = useColors();
const styles = React.useMemo(() => makeStyles(c), [c]);
const t = useLocaleStore((s) => s.t);
const pct = attachment.progress != null ? Math.round(attachment.progress * 100) : null;
return (
<Pressable onPress={onPress} style={styles.attachmentChip} disabled={attachment.uploading}>
@@ -286,7 +297,9 @@ function AttachmentChip({
{attachment.error
? attachment.error
: attachment.uploading
? pct != null ? `${uploadingLabel} ${pct}%` : uploadingLabel
? pct != null
? t('email_composer.uploading_pct', 'Uploading {pct}%', { pct })
: t('email_composer.uploading', 'Uploading...')
: formatBytes(attachment.size)}
</Text>
{attachment.uploading && pct != null && (
@@ -299,7 +312,10 @@ function AttachmentChip({
onPress={onRemove}
hitSlop={8}
style={styles.attachmentRemove}
accessibilityLabel={attachment.uploading ? cancelLabel : undefined}
accessibilityRole="button"
accessibilityLabel={attachment.uploading
? t('email_composer.upload_cancel', 'Cancel upload')
: t('email_composer.remove_attachment', 'Remove attachment')}
>
<X size={14} color={c.textMuted} />
</Pressable>
@@ -789,7 +805,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
if (!cancelled) {
updateAttachment(localId, {
uploading: false,
error: e instanceof Error ? e.message : 'Upload failed',
error: e instanceof Error ? e.message : t('email_composer.upload_failed_short', 'Upload failed'),
});
}
}
@@ -1543,7 +1559,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
});
} catch (e) {
if (e instanceof Error && e.name === 'AbortError') return; // removed by the user
const message = e instanceof Error ? e.message : 'Upload failed';
const message = e instanceof Error ? e.message : t('email_composer.upload_failed_short', 'Upload failed');
updateAttachment(entry.localId, { uploading: false, progress: undefined, error: message });
Alert.alert(
t('email_composer.upload_failed', 'Failed to upload {filename}', { filename: entry.name }),
@@ -2177,7 +2193,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
}
Alert.alert(
t('email_composer.send_failed', 'Send failed'),
e instanceof Error ? e.message : 'Failed to send email',
e instanceof Error ? e.message : t('notifications.error_sending', 'Failed to send email'),
);
} finally {
setSending(false);
@@ -2280,7 +2296,13 @@ export default function ComposeScreen({ route, navigation }: Props) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<View style={styles.header}>
<Pressable onPress={onClose} style={styles.headerBtn} disabled={savingDraft}>
<Pressable
onPress={onClose}
style={styles.headerBtn}
disabled={savingDraft}
accessibilityRole="button"
accessibilityLabel={t('common.close', 'Close')}
>
{savingDraft ? <ActivityIndicator size="small" color={c.text} /> : <X size={22} color={c.text} />}
</Pressable>
<View style={styles.headerTitleWrap}>
@@ -2296,6 +2318,8 @@ export default function ComposeScreen({ route, navigation }: Props) {
onPress={() => setAttachMenuOpen(true)}
style={styles.headerBtn}
hitSlop={8}
accessibilityRole="button"
accessibilityLabel={t('email_composer.attach', 'Attach')}
>
<Paperclip size={20} color={c.text} />
</Pressable>
@@ -2306,6 +2330,8 @@ export default function ComposeScreen({ route, navigation }: Props) {
style={styles.headerBtn}
hitSlop={8}
disabled={!canSend}
accessibilityRole="button"
accessibilityLabel={t('email_composer.schedule_send', 'Schedule send')}
>
<Clock size={20} color={canSend ? c.text : c.textMuted} />
</Pressable>
@@ -2352,7 +2378,13 @@ export default function ComposeScreen({ route, navigation }: Props) {
{identities.length > 1 && <ChevronDown size={14} color={c.textMuted} />}
</Pressable>
{!!primaryIdentity && (
<Pressable onPress={openFromOptions} hitSlop={8} style={styles.fromOptionsBtn}>
<Pressable
onPress={openFromOptions}
hitSlop={8}
style={styles.fromOptionsBtn}
accessibilityRole="button"
accessibilityLabel={t('email_composer.from_options', 'Sender options')}
>
<Tag size={16} color={subAddressTag || fromOverride ? c.primary : c.textMuted} />
</Pressable>
)}
@@ -2381,8 +2413,6 @@ export default function ComposeScreen({ route, navigation }: Props) {
attachment={a}
onRemove={() => removeAttachment(a.localId)}
onPress={() => { void previewAttachment(a); }}
uploadingLabel={t('email_composer.uploading', 'Uploading...')}
cancelLabel={t('email_composer.upload_cancel', 'Cancel upload')}
/>
))}
</View>
@@ -2790,7 +2820,11 @@ export default function ComposeScreen({ route, navigation }: Props) {
)}
<View style={styles.switchRow}>
<Text style={styles.switchLabel}>{t('email_composer.from_override.toggle_off', 'Override')}</Text>
<Switch value={overrideEnabled} onValueChange={setOverrideEnabled} />
<Switch
value={overrideEnabled}
onValueChange={setOverrideEnabled}
accessibilityLabel={t('email_composer.from_override.toggle_off', 'Override')}
/>
</View>
{overrideEnabled && (
<>