mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-04 00:06:53 +00:00
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:
@@ -226,6 +226,8 @@
|
|||||||
"editor_unavailable": "Could not read the message content. Copy your text, then close and reopen the composer.",
|
"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_plain_text": "Switch to plain text",
|
||||||
"format_rich_text": "Switch to rich text (HTML)",
|
"format_rich_text": "Switch to rich text (HTML)",
|
||||||
|
"from_options": "Sender options",
|
||||||
|
"group_chip": "{name} ({count})",
|
||||||
"identity_unavailable": "Identity unavailable",
|
"identity_unavailable": "Identity unavailable",
|
||||||
"image_load_failed": "Could not load image",
|
"image_load_failed": "Could not load image",
|
||||||
"no_recipient": "(no recipient)",
|
"no_recipient": "(no recipient)",
|
||||||
@@ -236,6 +238,7 @@
|
|||||||
"recipient_copy": "Copy address",
|
"recipient_copy": "Copy address",
|
||||||
"recipient_move_to": "Move to {field}",
|
"recipient_move_to": "Move to {field}",
|
||||||
"recipient_remove": "Remove",
|
"recipient_remove": "Remove",
|
||||||
|
"remove_attachment": "Remove attachment",
|
||||||
"save_draft": "Save Draft",
|
"save_draft": "Save Draft",
|
||||||
"save_failed_no_drafts": "There is no Drafts folder to save this draft to.",
|
"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.",
|
"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"
|
"remove_background_color": "Remove background color"
|
||||||
},
|
},
|
||||||
"undo_send_label": "Sending in {seconds}s…",
|
"undo_send_label": "Sending in {seconds}s…",
|
||||||
|
"upload_failed_short": "Upload failed",
|
||||||
"uploading": "Uploading...",
|
"uploading": "Uploading...",
|
||||||
|
"uploading_pct": "Uploading {pct}%",
|
||||||
"validation": {
|
"validation": {
|
||||||
"subject_required": "Add a subject"
|
"subject_required": "Add a subject"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ const TRANSLATED_FILES: Record<string, string[]> = {
|
|||||||
'components/files/FilePreviewModal.tsx': [],
|
'components/files/FilePreviewModal.tsx': [],
|
||||||
// The empty date filter shows the input format.
|
// The empty date filter shows the input format.
|
||||||
'screens/EmailListScreen.tsx': ['YYYY-MM-DD'],
|
'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/ScheduledScreen.tsx': [],
|
||||||
'screens/EmailSourceScreen.tsx': [],
|
'screens/EmailSourceScreen.tsx': [],
|
||||||
'components/OfflineBanner.tsx': [],
|
'components/OfflineBanner.tsx': [],
|
||||||
|
|||||||
@@ -192,8 +192,12 @@ function RecipientChip({
|
|||||||
}) {
|
}) {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||||
|
const t = useLocaleStore((s) => s.t);
|
||||||
const label = recipient.group
|
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;
|
: recipient.name || recipient.email;
|
||||||
return (
|
return (
|
||||||
<Pressable onLongPress={onLongPress} delayLongPress={350} style={[styles.chip, invalid && styles.chipInvalid]}>
|
<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}>
|
<Text style={[styles.chipText, invalid && styles.chipTextInvalid]} numberOfLines={1}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</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} />
|
<X size={12} color={invalid ? c.error : c.textMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -225,6 +234,7 @@ function SuggestionList({
|
|||||||
}) {
|
}) {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||||
|
const t = useLocaleStore((s) => s.t);
|
||||||
return (
|
return (
|
||||||
<View style={styles.suggestionBox}>
|
<View style={styles.suggestionBox}>
|
||||||
{suggestions.map((s, i) => (
|
{suggestions.map((s, i) => (
|
||||||
@@ -244,7 +254,9 @@ function SuggestionList({
|
|||||||
{s.name || s.email}
|
{s.name || s.email}
|
||||||
</Text>
|
</Text>
|
||||||
{s.group ? (
|
{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 && (
|
) : !!s.name && (
|
||||||
<Text style={styles.suggestionEmail} numberOfLines={1}>{s.email}</Text>
|
<Text style={styles.suggestionEmail} numberOfLines={1}>{s.email}</Text>
|
||||||
)}
|
)}
|
||||||
@@ -256,16 +268,15 @@ function SuggestionList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AttachmentChip({
|
function AttachmentChip({
|
||||||
attachment, onRemove, onPress, uploadingLabel, cancelLabel,
|
attachment, onRemove, onPress,
|
||||||
}: {
|
}: {
|
||||||
attachment: AttachmentEntry;
|
attachment: AttachmentEntry;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
uploadingLabel: string;
|
|
||||||
cancelLabel: string;
|
|
||||||
}) {
|
}) {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||||
|
const t = useLocaleStore((s) => s.t);
|
||||||
const pct = attachment.progress != null ? Math.round(attachment.progress * 100) : null;
|
const pct = attachment.progress != null ? Math.round(attachment.progress * 100) : null;
|
||||||
return (
|
return (
|
||||||
<Pressable onPress={onPress} style={styles.attachmentChip} disabled={attachment.uploading}>
|
<Pressable onPress={onPress} style={styles.attachmentChip} disabled={attachment.uploading}>
|
||||||
@@ -286,7 +297,9 @@ function AttachmentChip({
|
|||||||
{attachment.error
|
{attachment.error
|
||||||
? attachment.error
|
? attachment.error
|
||||||
: attachment.uploading
|
: 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)}
|
: formatBytes(attachment.size)}
|
||||||
</Text>
|
</Text>
|
||||||
{attachment.uploading && pct != null && (
|
{attachment.uploading && pct != null && (
|
||||||
@@ -299,7 +312,10 @@ function AttachmentChip({
|
|||||||
onPress={onRemove}
|
onPress={onRemove}
|
||||||
hitSlop={8}
|
hitSlop={8}
|
||||||
style={styles.attachmentRemove}
|
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} />
|
<X size={14} color={c.textMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -789,7 +805,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
|||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
updateAttachment(localId, {
|
updateAttachment(localId, {
|
||||||
uploading: false,
|
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) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.name === 'AbortError') return; // removed by the user
|
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 });
|
updateAttachment(entry.localId, { uploading: false, progress: undefined, error: message });
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
t('email_composer.upload_failed', 'Failed to upload {filename}', { filename: entry.name }),
|
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(
|
Alert.alert(
|
||||||
t('email_composer.send_failed', 'Send failed'),
|
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 {
|
} finally {
|
||||||
setSending(false);
|
setSending(false);
|
||||||
@@ -2280,7 +2296,13 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
<SafeAreaView style={styles.container} edges={['top']}>
|
||||||
<View style={styles.header}>
|
<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} />}
|
{savingDraft ? <ActivityIndicator size="small" color={c.text} /> : <X size={22} color={c.text} />}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<View style={styles.headerTitleWrap}>
|
<View style={styles.headerTitleWrap}>
|
||||||
@@ -2296,6 +2318,8 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
|||||||
onPress={() => setAttachMenuOpen(true)}
|
onPress={() => setAttachMenuOpen(true)}
|
||||||
style={styles.headerBtn}
|
style={styles.headerBtn}
|
||||||
hitSlop={8}
|
hitSlop={8}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={t('email_composer.attach', 'Attach')}
|
||||||
>
|
>
|
||||||
<Paperclip size={20} color={c.text} />
|
<Paperclip size={20} color={c.text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -2306,6 +2330,8 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
|||||||
style={styles.headerBtn}
|
style={styles.headerBtn}
|
||||||
hitSlop={8}
|
hitSlop={8}
|
||||||
disabled={!canSend}
|
disabled={!canSend}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={t('email_composer.schedule_send', 'Schedule send')}
|
||||||
>
|
>
|
||||||
<Clock size={20} color={canSend ? c.text : c.textMuted} />
|
<Clock size={20} color={canSend ? c.text : c.textMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -2352,7 +2378,13 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
|||||||
{identities.length > 1 && <ChevronDown size={14} color={c.textMuted} />}
|
{identities.length > 1 && <ChevronDown size={14} color={c.textMuted} />}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{!!primaryIdentity && (
|
{!!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} />
|
<Tag size={16} color={subAddressTag || fromOverride ? c.primary : c.textMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
@@ -2381,8 +2413,6 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
|||||||
attachment={a}
|
attachment={a}
|
||||||
onRemove={() => removeAttachment(a.localId)}
|
onRemove={() => removeAttachment(a.localId)}
|
||||||
onPress={() => { void previewAttachment(a); }}
|
onPress={() => { void previewAttachment(a); }}
|
||||||
uploadingLabel={t('email_composer.uploading', 'Uploading...')}
|
|
||||||
cancelLabel={t('email_composer.upload_cancel', 'Cancel upload')}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
@@ -2790,7 +2820,11 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
|||||||
)}
|
)}
|
||||||
<View style={styles.switchRow}>
|
<View style={styles.switchRow}>
|
||||||
<Text style={styles.switchLabel}>{t('email_composer.from_override.toggle_off', 'Override')}</Text>
|
<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>
|
</View>
|
||||||
{overrideEnabled && (
|
{overrideEnabled && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user